From a4d42f1b316859c1735b6fa5a00337cc1b8eed39 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 23 Nov 2020 11:46:16 +0000 Subject: [PATCH 01/23] pin cftime<1.3.0 (#3927) --- requirements/ci/py36.yml | 2 +- requirements/ci/py37.yml | 2 +- requirements/core.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/ci/py36.yml b/requirements/ci/py36.yml index 9e6d76c9318..0461cdf880c 100644 --- a/requirements/ci/py36.yml +++ b/requirements/ci/py36.yml @@ -13,7 +13,7 @@ dependencies: # Core dependencies. - cartopy>=0.18 - cf-units>=2 - - cftime + - cftime<1.3.0 - dask>=2 - matplotlib - netcdf4 diff --git a/requirements/ci/py37.yml b/requirements/ci/py37.yml index 4c9825a97d1..8817f575b7b 100644 --- a/requirements/ci/py37.yml +++ b/requirements/ci/py37.yml @@ -13,7 +13,7 @@ dependencies: # Core dependencies. - cartopy>=0.18 - cf-units>=2 - - cftime + - cftime<1.3.0 - dask>=2 - matplotlib - netcdf4 diff --git a/requirements/core.txt b/requirements/core.txt index 0b59c573ec1..9e0c4fb1bbb 100644 --- a/requirements/core.txt +++ b/requirements/core.txt @@ -2,7 +2,7 @@ cartopy>=0.18 cf-units>=2 -cftime +cftime<1.3.0 dask[array]>=2 matplotlib netcdf4 From a2679186046dfe1b6e0b419d093eae836c6896d9 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 25 Nov 2020 14:46:09 +0000 Subject: [PATCH 02/23] Merge back v3.0.x (#3930) * Add release highlights and pin rc version (#3898) * Add release highlights and pin rc version * review actions * reorder release highlights (#3899) Tweak release highlights * Add whatsnew announcement (#3900) * Fix spelling (#3903) * Fix unit label handling (#3902) * Add failing test of plotting * Implement fix to pass test * Update idiff to ignore irrelevant hyphens in path * Update imagerepo (following docs) * Update after review by @trexfeathers * Add whatsnew entries * Move whatsnew entries into correct file * Release Docs Improvements (#3895) * Minor phrasing change in 'Release candidate'. * Before release deprecations. * Whatsnew highlights section. * Relax setup.py setup requirements (#3909) * Updated CF saver version in User Guide and docstring (#3925) * Updated CF saver version in User Guide and docstring * Remove references to CF version of the loader in docstrings * Added whatsnew * Pin cftime<1.3.0 * Migrate to cirrus-ci (#3928) * migrate from travis-ci to cirrus-ci * added whatsnew entries * ignore url for doc link check (#3929) * whatsnew for coord default units (#3924) * merge back v3.0.x branch Co-authored-by: tkknight <2108488+tkknight@users.noreply.github.com> Co-authored-by: Zeb Nicholls Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Co-authored-by: Jon Seddon <17068361+jonseddon@users.noreply.github.com> Co-authored-by: Ruth Comer --- .cirrus.yml | 227 +++++++++++++ .gitignore | 1 + .stickler.yml | 4 - .travis.yml | 169 ---------- README.md | 14 +- docs/iris/src/common_links.inc | 1 + docs/iris/src/conf.py | 11 +- docs/iris/src/developers_guide/release.rst | 37 ++- .../src/further_topics/lenient_metadata.rst | 4 +- docs/iris/src/userguide/plotting_a_cube.rst | 2 +- docs/iris/src/userguide/saving_iris_cubes.rst | 2 +- docs/iris/src/whatsnew/3.0.rst | 66 +++- lib/iris/fileformats/cf.py | 2 +- lib/iris/fileformats/netcdf.py | 5 +- lib/iris/quickplot.py | 2 +- lib/iris/tests/idiff.py | 4 +- lib/iris/tests/results/analysis/sqrt.cml | 4 +- lib/iris/tests/results/imagerepo.json | 3 + lib/iris/tests/test_basic_maths.py | 7 +- lib/iris/tests/test_coding_standards.py | 1 + lib/iris/tests/test_netcdf.py | 25 +- lib/iris/tests/test_quickplot.py | 7 + noxfile.py | 312 ++++++++++++++++++ requirements/ci/py36.yml | 1 + requirements/ci/py37.yml | 1 + requirements/setup.txt | 2 - setup.cfg | 44 --- 27 files changed, 686 insertions(+), 272 deletions(-) create mode 100644 .cirrus.yml delete mode 100644 .stickler.yml delete mode 100644 .travis.yml create mode 100644 noxfile.py delete mode 100644 setup.cfg diff --git a/.cirrus.yml b/.cirrus.yml new file mode 100644 index 00000000000..d4aedd39555 --- /dev/null +++ b/.cirrus.yml @@ -0,0 +1,227 @@ +# Reference: +# - https://cirrus-ci.org/guide/writing-tasks/ +# - https://cirrus-ci.org/guide/tips-and-tricks/#sharing-configuration-between-tasks +# - https://cirrus-ci.org/guide/linux/ +# - https://cirrus-ci.org/guide/macOS/ +# - https://cirrus-ci.org/guide/windows/ +# - https://hub.docker.com/_/gcc/ +# - https://hub.docker.com/_/python/ + +# +# Global defaults. +# +container: + image: python:3.8 + cpu: 2 + memory: 4G + + +env: + # Maximum cache period (in weeks) before forcing a new cache upload. + CACHE_PERIOD: "2" + # Increment the build number to force new cartopy cache upload. + CARTOPY_CACHE_BUILD: "0" + # Increment the build number to force new conda cache upload. + CONDA_CACHE_BUILD: "0" + # Increment the build number to force new nox cache upload. + NOX_CACHE_BUILD: "0" + # Increment the build number to force new pip cache upload. + PIP_CACHE_BUILD: "0" + # Pip package to be upgraded/installed. + PIP_CACHE_PACKAGES: "pip setuptools wheel nox" + # Git commit hash for iris test data. + IRIS_TEST_DATA_REF: "fffb9b14b9cb472c5eb2ebb7fd19acb7f6414a30" + # Base directory for the iris-test-data. + IRIS_TEST_DATA_DIR: ${HOME}/iris-test-data + + +# +# Linting +# +lint_task: + auto_cancellation: true + name: "${CIRRUS_OS}: flake8 and black" + pip_cache: + folder: ~/.cache/pip + fingerprint_script: + - echo "${CIRRUS_TASK_NAME}" + - echo "$(date +%Y).$(($(date +%U) / ${CACHE_PERIOD})):${PIP_CACHE_BUILD} ${PIP_CACHE_PACKAGES}" + lint_script: + - pip list + - python -m pip install --retries 3 --upgrade ${PIP_CACHE_PACKAGES} + - pip list + - nox --session flake8 + - nox --session black + + +# +# YAML alias for common linux test infra-structure. +# +linux_task_template: &LINUX_TASK_TEMPLATE + auto_cancellation: true + env: + IRIS_REPO_DIR: ${CIRRUS_WORKING_DIR} + PATH: ${HOME}/miniconda/bin:${PATH} + SITE_CFG: ${CIRRUS_WORKING_DIR}/lib/iris/etc/site.cfg + conda_cache: + folder: ${HOME}/miniconda + fingerprint_script: + - wget --quiet https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh + - echo "${CIRRUS_OS} $(sha256sum miniconda.sh)" + - echo "$(date +%Y).$(($(date +%U) / ${CACHE_PERIOD})):${CONDA_CACHE_BUILD}" + populate_script: + - bash miniconda.sh -b -p ${HOME}/miniconda + - conda config --set always_yes yes --set changeps1 no + - conda config --set show_channel_urls True + - conda config --add channels conda-forge + - conda update --quiet --name base conda + - conda install --quiet --name base nox pip + cartopy_cache: + folder: ${HOME}/.local/share/cartopy + fingerprint_script: + - echo "${CIRRUS_OS}" + - echo "$(date +%Y).$(($(date +%U) / ${CACHE_PERIOD})):${CARTOPY_CACHE_BUILD}" + nox_cache: + folder: ${CIRRUS_WORKING_DIR}/.nox + fingerprint_script: + - echo "${CIRRUS_TASK_NAME}" + - echo "$(date +%Y).$(($(date +%U) / ${CACHE_PERIOD})):${NOX_CACHE_BUILD}" + - sha256sum ${CIRRUS_WORKING_DIR}/requirements/ci/py$(echo ${PY_VER} | tr -d ".").yml + + +# +# Testing Minimal (Linux) +# +linux_minimal_task: + matrix: + env: + PY_VER: 3.6 + env: + PY_VER: 3.7 + name: "${CIRRUS_OS}: py${PY_VER} tests (minimal)" + container: + image: gcc:latest + cpu: 2 + memory: 4G + << : *LINUX_TASK_TEMPLATE + tests_script: + - echo "[Resources]" > ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - nox --session tests + + +# +# Testing Full (Linux) +# +linux_task: + matrix: + env: + PY_VER: 3.6 + env: + PY_VER: 3.7 + name: "${CIRRUS_OS}: py${PY_VER} tests (full)" + container: + image: gcc:latest + cpu: 6 + memory: 8G + data_cache: + folder: ${IRIS_TEST_DATA_DIR} + fingerprint_script: + - echo "${IRIS_TEST_DATA_REF}" + populate_script: + - wget --quiet https://github.com/SciTools/iris-test-data/archive/${IRIS_TEST_DATA_REF}.zip -O iris-test-data.zip + - unzip -q iris-test-data.zip + - mv iris-test-data-$(echo "${IRIS_TEST_DATA_REF}" | sed "s/^v//") ${IRIS_TEST_DATA_DIR} + << : *LINUX_TASK_TEMPLATE + tests_script: + - echo "[Resources]" > ${SITE_CFG} + - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - nox --session tests + + +# +# Testing Documentation Gallery (Linux) +# +gallery_task: + matrix: + env: + PY_VER: 3.6 + env: + PY_VER: 3.7 + name: "${CIRRUS_OS}: py${PY_VER} doc tests (gallery)" + container: + image: gcc:latest + cpu: 2 + memory: 4G + data_cache: + folder: ${IRIS_TEST_DATA_DIR} + fingerprint_script: + - echo "${IRIS_TEST_DATA_REF}" + populate_script: + - wget --quiet https://github.com/SciTools/iris-test-data/archive/${IRIS_TEST_DATA_REF}.zip -O iris-test-data.zip + - unzip -q iris-test-data.zip + - mv iris-test-data-$(echo "${IRIS_TEST_DATA_REF}" | sed "s/^v//") ${IRIS_TEST_DATA_DIR} + << : *LINUX_TASK_TEMPLATE + tests_script: + - echo "[Resources]" > ${SITE_CFG} + - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - nox --session gallery + + +# +# Testing Documentation (Linux) +# +doctest_task: + matrix: + env: + PY_VER: 3.7 + name: "${CIRRUS_OS}: py${PY_VER} doc tests" + container: + image: gcc:latest + cpu: 2 + memory: 4G + env: + MPL_RC_DIR: ${HOME}/.config/matplotlib + MPL_RC_FILE: ${HOME}/.config/matplotlib/matplotlibrc + data_cache: + folder: ${IRIS_TEST_DATA_DIR} + fingerprint_script: + - echo "${IRIS_TEST_DATA_REF}" + populate_script: + - wget --quiet https://github.com/SciTools/iris-test-data/archive/${IRIS_TEST_DATA_REF}.zip -O iris-test-data.zip + - unzip -q iris-test-data.zip + - mv iris-test-data-$(echo "${IRIS_TEST_DATA_REF}" | sed "s/^v//") ${IRIS_TEST_DATA_DIR} + << : *LINUX_TASK_TEMPLATE + tests_script: + - echo "[Resources]" > ${SITE_CFG} + - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - mkdir -p ${MPL_RC_DIR} + - echo "backend : agg" > ${MPL_RC_FILE} + - echo "image.cmap : viridis" >> ${MPL_RC_FILE} + - nox --session doctest + + +# +# Testing Documentation Link Check (Linux) +# +link_task: + matrix: + env: + PY_VER: 3.7 + name: "${CIRRUS_OS}: py${PY_VER} doc link check" + container: + image: gcc:latest + cpu: 2 + memory: 4G + env: + MPL_RC_DIR: ${HOME}/.config/matplotlib + MPL_RC_FILE: ${HOME}/.config/matplotlib/matplotlibrc + << : *LINUX_TASK_TEMPLATE + tests_script: + - mkdir -p ${MPL_RC_DIR} + - echo "backend : agg" > ${MPL_RC_FILE} + - echo "image.cmap : viridis" >> ${MPL_RC_FILE} + - nox --session linkcheck diff --git a/.gitignore b/.gitignore index d589c306fea..618913e7ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ var sdist develop-eggs .installed.cfg +.nox # Installer logs pip-log.txt diff --git a/.stickler.yml b/.stickler.yml deleted file mode 100644 index 6edee0f6a50..00000000000 --- a/.stickler.yml +++ /dev/null @@ -1,4 +0,0 @@ -linters: - flake8: - python: 3 - config: ./.flake8 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ab1accab4a7..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,169 +0,0 @@ -# Please update the test data git references below if appropriate. -# -# Note: Contrary to the travis documentation, -# http://about.travis-ci.org/docs/user/languages/python/#Travis-CI-Uses-Isolated-virtualenvs -# we will use conda to give us a much faster setup time. - -language: minimal -dist: xenial - -env: - global: - # The decryption key for the encrypted .github/deploy_key.scitools-docs.enc. - - secure: "N9/qBUT5CqfC7KQBDy5mIWZcGNuUJk3e/qmKJpotWYV+zwOI4GghJsRce6nFnlRiwl65l5oBEcvf3+sBvUfbZqh7U0MdHpw2tHhr2FSCmMB3bkvARZblh9M37f4da9G9VmRkqnyBM5G5TImXtoq4dusvNWKvLW0qETciaipq7ws=" - matrix: - - PYTHON_VERSION='36' TEST_TARGET='default' TEST_MINIMAL=true - - PYTHON_VERSION='36' TEST_TARGET='default' TEST_BLACK=true - - PYTHON_VERSION='36' TEST_TARGET='gallery' - - PYTHON_VERSION='37' TEST_TARGET='default' TEST_MINIMAL=true - - PYTHON_VERSION='37' TEST_TARGET='default' TEST_BLACK=true - - PYTHON_VERSION='37' TEST_TARGET='gallery' - - PYTHON_VERSION='37' TEST_TARGET='doctest' PUSH_BUILT_DOCS=true - - PYTHON_VERSION='37' TEST_TARGET='linkcheck' - # TODO: Dependencies for sphinxcontrib-spelling to be in place before this - # spelling code block is enabled - #- PYTHON_VERSION='37' TEST_TARGET='spelling' - -git: - # We need a deep clone so that we can compute the age of the files using their git history. - depth: 10000 - -install: - - > - export IRIS_TEST_DATA_REF="fffb9b14b9cb472c5eb2ebb7fd19acb7f6414a30"; - export IRIS_TEST_DATA_SUFFIX=$(echo "${IRIS_TEST_DATA_REF}" | sed "s/^v//"); - - # Install miniconda - # ----------------- - - > - echo 'Installing miniconda'; - export CONDA_BASE="https://repo.continuum.io/miniconda/Miniconda"; - wget --quiet ${CONDA_BASE}3-latest-Linux-x86_64.sh -O miniconda.sh; - bash miniconda.sh -b -p ${HOME}/miniconda; - export PATH="${HOME}/miniconda/bin:${PATH}"; - - # Create the testing environment - # ------------------------------ - # Explicitly add defaults channel, see https://github.com/conda/conda/issues/2675 - - > - echo 'Configure conda and create an environment'; - conda config --set always_yes yes --set changeps1 no; - conda config --set show_channel_urls True; - conda config --add channels conda-forge; - conda update --quiet conda; - export ENV_NAME='iris-dev'; - ENV_FILE="requirements/ci/py${PYTHON_VERSION}.yml"; - cat ${ENV_FILE}; - conda env create --quiet --file=${ENV_FILE}; - source activate ${ENV_NAME}; - export PREFIX="${CONDA_PREFIX}"; - - # Output debug info - - > - conda list -n ${ENV_NAME}; - conda list -n ${ENV_NAME} --explicit; - conda info -a; - -# Pre-load Natural Earth data to avoid multiple, overlapping downloads. -# i.e. There should be no DownloadWarning reports in the log. - - python -c 'import cartopy; cartopy.io.shapereader.natural_earth()' - -# iris test data - - > - if [[ "${TEST_MINIMAL}" != true ]]; then - wget --quiet -O iris-test-data.zip https://github.com/SciTools/iris-test-data/archive/${IRIS_TEST_DATA_REF}.zip; - unzip -q iris-test-data.zip; - mv "iris-test-data-${IRIS_TEST_DATA_SUFFIX}" iris-test-data; - fi - -# set config paths - - > - SITE_CFG="lib/iris/etc/site.cfg"; - echo "[Resources]" > ${SITE_CFG}; - echo "test_data_dir = $(pwd)/iris-test-data/test_data" >> ${SITE_CFG}; - echo "doc_dir = $(pwd)/docs/iris" >> ${SITE_CFG}; - echo "[System]" >> ${SITE_CFG}; - echo "udunits2_path = ${PREFIX}/lib/libudunits2.so" >> ${SITE_CFG}; - - - python setup.py --quiet install - -script: - # Capture install-dir: As a test command must be last for get Travis to check - # the RC, so it's best to start each operation with an absolute cd. - - export INSTALL_DIR=$(pwd) - - - > - if [[ "${TEST_BLACK}" == 'true' ]]; then - echo $(black --version); - rm ${INSTALL_DIR}/.gitignore; - black --check ${INSTALL_DIR}; - fi - - - > - if [[ "${TEST_TARGET}" == 'default' ]]; then - export IRIS_REPO_DIR=${INSTALL_DIR}; - python -m iris.tests.runner --default-tests --system-tests; - fi - - - > - if [[ "${TEST_TARGET}" == 'gallery' ]]; then - python -m iris.tests.runner --gallery-tests; - fi - - # Build the docs. - - > - if [[ "${TEST_TARGET}" == 'doctest' ]]; then - MPL_RC_DIR="${HOME}/.config/matplotlib"; - mkdir -p ${MPL_RC_DIR}; - echo 'backend : agg' > ${MPL_RC_DIR}/matplotlibrc; - echo 'image.cmap : viridis' >> ${MPL_RC_DIR}/matplotlibrc; - cd ${INSTALL_DIR}/docs/iris; - make clean html && make doctest; - fi - - # check the links in the docs - - > - if [[ "${TEST_TARGET}" == 'linkcheck' ]]; then - MPL_RC_DIR="${HOME}/.config/matplotlib"; - mkdir -p ${MPL_RC_DIR}; - echo 'backend : agg' > ${MPL_RC_DIR}/matplotlibrc; - echo 'image.cmap : viridis' >> ${MPL_RC_DIR}/matplotlibrc; - cd ${INSTALL_DIR}/docs/iris; - make clean && make linkcheck; - fi - - # TODO: Dependencies for sphinxcontrib-spelling to be in place before this - # spelling code block is enabled - - # check the spelling in the docs - # - > - # if [[ "${TEST_TARGET}" == 'spelling' ]]; then - # MPL_RC_DIR="${HOME}/.config/matplotlib"; - # mkdir -p ${MPL_RC_DIR}; - # echo 'backend : agg' > ${MPL_RC_DIR}/matplotlibrc; - # echo 'image.cmap : viridis' >> ${MPL_RC_DIR}/matplotlibrc; - # cd ${INSTALL_DIR}/docs/iris; - # make clean && make spelling; - # fi - - # Split the organisation out of the slug. See https://stackoverflow.com/a/5257398/741316 for description. - # NOTE: a *separate* "export" command appears to be necessary here : A command of the - # form "export ORG=.." failed to define ORG for the following command (?!) - - > - ORG=$(echo ${TRAVIS_REPO_SLUG} | cut -d/ -f1); - export ORG - - - echo "Travis job context ORG=${ORG}; TRAVIS_EVENT_TYPE=${TRAVIS_EVENT_TYPE}; PUSH_BUILT_DOCS=${PUSH_BUILT_DOCS}" - - # When we merge a change to SciTools/iris, we can push docs to github pages. - # At present, only the Python 3.7 "doctest" job does this. - # Results appear at https://scitools-docs.github.io/iris/<>/index.html - - if [[ "${ORG}" == 'SciTools' && "${TRAVIS_EVENT_TYPE}" == 'push' && "${PUSH_BUILT_DOCS}" == 'true' ]]; then - cd ${INSTALL_DIR}; - conda install --quiet -n ${ENV_NAME} pip; - pip install doctr; - doctr deploy --deploy-repo SciTools-docs/iris --built-docs docs/iris/src/_build/html - --key-path .github/deploy_key.scitools-docs.enc - --no-require-master - ${TRAVIS_BRANCH:-${TRAVIS_TAG}}; - fi diff --git a/README.md b/README.md index aeadb52d936..6339491955f 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,12 @@

- - -Travis-CI + +Cirrus-CI - Documentation Status +Documentation Status conda-forge downloads @@ -26,9 +25,6 @@ Latest version - -Stable docs Commits since last release diff --git a/docs/iris/src/common_links.inc b/docs/iris/src/common_links.inc index 94c2f3c92bd..0bc8ca60e61 100644 --- a/docs/iris/src/common_links.inc +++ b/docs/iris/src/common_links.inc @@ -25,3 +25,4 @@ .. _sphinx: https://www.sphinx-doc.org/en/master/ .. _napolean: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/sphinxcontrib.napoleon.html .. _legacy documentation: https://scitools.org.uk/iris/docs/v2.4.0/ +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 9e6276f5445..ab7689479a9 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -261,13 +261,14 @@ def autolog(message): # url link checker. Some links work but report as broken, lets ignore them. # See https://www.sphinx-doc.org/en/1.2/config.html#options-for-the-linkcheck-builder linkcheck_ignore = [ - "https://github.com/SciTools/iris/commit/69597eb3d8501ff16ee3d56aef1f7b8f1c2bb316#diff-1680206bdc5cfaa83e14428f5ba0f848", - "http://www.wmo.int/pages/prog/www/DPFS/documents/485_Vol_I_en_colour.pdf", + "http://cfconventions.org", "http://code.google.com/p/msysgit/downloads/list", + "http://effbot.org", + "https://github.com", + "http://www.personal.psu.edu/cab38/ColorBrewer/ColorBrewer_updates.html", "http://schacon.github.com/git", - "https://github.com/SciTools/iris/pull", - "https://github.com/SciTools/iris/issue", - "http://cfconventions.org", + "http://scitools.github.com/cartopy", + "http://www.wmo.int/pages/prog/www/DPFS/documents/485_Vol_I_en_colour.pdf", ] # list of sources to exclude from the build. diff --git a/docs/iris/src/developers_guide/release.rst b/docs/iris/src/developers_guide/release.rst index d71f1491861..2ec787a7806 100644 --- a/docs/iris/src/developers_guide/release.rst +++ b/docs/iris/src/developers_guide/release.rst @@ -3,17 +3,28 @@ Releases ======== -A release of Iris is a `tag on the SciTools/Iris`_ +A release of Iris is a `tag on the SciTools/Iris`_ Github repository. The summary below is of the main areas that constitute the release. The final section details the :ref:`iris_development_releases_steps` to take. +Before release +-------------- + +Deprecations +~~~~~~~~~~~~ + +Ensure that any behaviour which has been deprecated for the correct number of +previous releases is now finally changed. More detail, including the correct +number of releases, is in :ref:`iris_development_deprecations`. + + Release branch -------------- -Once the features intended for the release are on master, a release branch +Once the features intended for the release are on master, a release branch should be created, in the SciTools/Iris repository. This will have the name: :literal:`v{major release number}.{minor release number}.x` @@ -35,12 +46,12 @@ number, e.g.: :literal:`v1.9.0rc1` -If created, the pre-release shall be available for a minimum of two weeks +If created, the pre-release shall be available for a minimum of two weeks prior to the release being cut. However a 4 week period should be the goal to allow user groups to be notified of the existence of the pre-release and encouraged to test the functionality. -A pre-release is expected for a minor release, but will not for a +A pre-release is expected for a major or minor release, but not for a point release. If new features are required for a release after a release candidate has been @@ -59,7 +70,7 @@ Steps to achieve this can be found in the :ref:`iris_development_releases_steps` The release ----------- -The final steps are to change the version string in the source of +The final steps are to change the version string in the source of :literal:`Iris.__init__.py` and include the release date in the relevant what's new page within the documentation. @@ -72,7 +83,7 @@ Conda recipe Once a release is cut, the `Iris feedstock`_ for the conda recipe must be updated to build the latest release of Iris and push this artefact to -`conda forge`_. +`conda forge`_. .. _Iris feedstock: https://github.com/conda-forge/iris-feedstock/tree/master/recipe .. _conda forge: https://anaconda.org/conda-forge/iris @@ -102,7 +113,7 @@ New features shall not be included in a point release, these are for bug fixes. A point release does not require a release candidate, but the rest of the release process is to be followed, including the merge back of changes into -:literal:`master`. +:literal:`master`. .. _iris_development_releases_steps: @@ -118,7 +129,7 @@ Release steps #. Create the branch ``1.9.x`` on the main repo, not in a forked repo, for the release candidate or release. The only exception is for a point/bugfix release as it should already exist -#. Update the what's new for the release: +#. Update the what's new for the release: * Copy ``docs/iris/src/whatsnew/latest.rst`` to a file named ``v1.9.rst`` @@ -128,6 +139,8 @@ Release steps the date and version in the format of ``v1.9 (DD MMM YYYY)``. For example ``v1.9 (03 Aug 2020)`` * Review the file for correctness + * Work with the development team to create a 'highlights' section at the + top of the file, providing extra detail on notable changes * Add ``v1.9.rst`` to git and commit all changes, including removal of ``latest.rst`` @@ -138,7 +151,7 @@ Release steps #. Update the ``Iris.__init__.py`` version string, to ``1.9.0`` #. Check your changes by building the documentation and viewing the changes -#. Once all the above steps are complete, the release is cut, using +#. Once all the above steps are complete, the release is cut, using the :guilabel:`Draft a new release` button on the `Iris release page `_ @@ -146,16 +159,16 @@ Release steps Post release steps ~~~~~~~~~~~~~~~~~~ -#. Check the documentation has built on `Read The Docs`_. The build is +#. Check the documentation has built on `Read The Docs`_. The build is triggered by any commit to master. Additionally check that the versions available in the pop out menu in the bottom left corner include the new release version. If it is not present you will need to configure the versions available in the **admin** dashboard in Read The Docs -#. Copy ``docs/iris/src/whatsnew/latest.rst.template`` to +#. Copy ``docs/iris/src/whatsnew/latest.rst.template`` to ``docs/iris/src/whatsnew/latest.rst``. This will reset the file with the ``unreleased`` heading and placeholders for the what's new headings -#. Add back in the reference to ``latest.rst`` to the what's new index +#. Add back in the reference to ``latest.rst`` to the what's new index ``docs/iris/src/whatsnew/index.rst`` #. Update ``Iris.__init__.py`` version string to show as ``1.10.dev0`` #. Merge back to master diff --git a/docs/iris/src/further_topics/lenient_metadata.rst b/docs/iris/src/further_topics/lenient_metadata.rst index 1b31759d9a1..ada70497863 100644 --- a/docs/iris/src/further_topics/lenient_metadata.rst +++ b/docs/iris/src/further_topics/lenient_metadata.rst @@ -335,10 +335,10 @@ Lenient combination The behaviour of the lenient ``combine`` metadata class method is outlined in :numref:`lenient combine table`, and as with :ref:`lenient equality` and -:ref:`lenient difference` is enabled throught the ``lenient`` keyword argument. +:ref:`lenient difference` is enabled through the ``lenient`` keyword argument. The difference in behaviour between **lenient** and -:ref:`strict combination ` is centered around the lenient +:ref:`strict combination ` is centred around the lenient handling of combining **something** with **nothing** (``None``) to return **something**. Whereas strict combination will only return a result from combining identical objects. diff --git a/docs/iris/src/userguide/plotting_a_cube.rst b/docs/iris/src/userguide/plotting_a_cube.rst index f646aa4b3ef..9de20dc6c9c 100644 --- a/docs/iris/src/userguide/plotting_a_cube.rst +++ b/docs/iris/src/userguide/plotting_a_cube.rst @@ -209,7 +209,7 @@ the temperature at some latitude cross-sections. ``_. In order to run this example, you will need to copy the code into a file - and run it using ``python2.7 my_file.py``. + and run it using ``python my_file.py``. Plotting 2-dimensional cubes diff --git a/docs/iris/src/userguide/saving_iris_cubes.rst b/docs/iris/src/userguide/saving_iris_cubes.rst index cca8b44bd16..3a30321979c 100644 --- a/docs/iris/src/userguide/saving_iris_cubes.rst +++ b/docs/iris/src/userguide/saving_iris_cubes.rst @@ -6,7 +6,7 @@ Saving Iris cubes Iris supports the saving of cubes and cube lists to: -* CF netCDF (version 1.6) +* CF netCDF (version 1.7) * GRIB edition 2 (if `iris-grib `_ is installed) * Met Office PP diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 0caba69de8c..0a9dcd89b0a 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -1,12 +1,45 @@ .. include:: ../common_links.inc -v3.0 (01 Oct 2020) +v3.0 (02 Oct 2020) ****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) +.. dropdown:: :opticon:`report` Release Highlights + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The highlights for this major release of Iris include: + + * We've finally dropped support for ``Python 2``, so welcome to ``Iris 3`` + and ``Python 3``! + * We've extended our coverage of the `CF Conventions and Metadata`_ by + introducing support for `CF Ancillary Data`_ and `Quality Flags`_, + * Lazy regridding is now available for several regridding schemes, + * Managing and manipulating metadata within Iris is now easier and more + consistent thanks to the introduction of a new common metadata API, + * :ref:`Cube arithmetic ` has been significantly improved with + regards to extended broadcasting, auto-transposition and a more lenient + behaviour towards handling metadata and coordinates, + * Our :ref:`documentation ` has been refreshed, + restructured, revitalised and rehosted on `readthedocs`_, + * It's now easier than ever to :ref:`install Iris ` + as a user or a developer, and the newly revamped developers guide walks + you though how you can :ref:`get involved ` + and contribute to Iris, + * Also, this is a major release of Iris, so please be aware of the + :ref:`incompatible changes ` and + :ref:`deprecations `. + + And finally, get in touch with us on `GitHub`_ if you have any issues or + feature requests for improving Iris. Enjoy! + + 📢 Announcements ================ @@ -16,6 +49,9 @@ This document explains the changes made to Iris for this release and performance metrics tool for routine evaluation of Earth system models in CMIP*". Welcome aboard! 🎉 +* Congratulations also goes to `@jonseddon`_ who recently became an Iris core + developer. We look forward to seeing more of your awesome contributions! 🎉 + ✨ Features =========== @@ -132,6 +168,9 @@ This document explains the changes made to Iris for this release Previously, the first tick label would occasionally be duplicated. This also removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) +* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) + +.. _whatsnew 3.0 changes: 💣 Incompatible Changes ======================= @@ -166,6 +205,9 @@ This document explains the changes made to Iris for this release :func:`iris.experimental.concatenate.concatenate` function raised an exception. (:pull:`3523`) +* `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` + and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) + * `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` property. Previously these cases defaulted to ``units='1'``. @@ -191,6 +233,8 @@ This document explains the changes made to Iris for this release exception was raised. (:pull:`3785`) +.. _whatsnew 3.0 deprecations: + 🔥 Deprecations =============== @@ -238,6 +282,8 @@ This document explains the changes made to Iris for this release dependency group. We no longer consider it to be an extension. (:pull:`3762`) +.. _whatsnew 3.0 docs: + 📚 Documentation ================ @@ -308,6 +354,8 @@ This document explains the changes made to Iris for this release included documentation for :ref:`metadata`, :ref:`lenient metadata`, and :ref:`lenient maths`. (:pull:`3890`) +* `@jonseddon`_ updated the CF version of the netCDF saver in the + :ref:`saving_iris_cubes` section and in the equivalent function docstring. 💼 Internal =========== @@ -375,6 +423,16 @@ This document explains the changes made to Iris for this release * `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) +* `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). + +* `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). + +* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_. (:pull:`3928`) + +* `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. + It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to + run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris + with `flake8`_ and `black`_. (:pull:`3928`) .. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ .. _Matplotlib: https://matplotlib.org/ @@ -408,6 +466,7 @@ This document explains the changes made to Iris for this release .. _@rcomer: https://github.com/rcomer .. _@jvegasbsc: https://github.com/jvegasbsc .. _@zklaus: https://github.com/zklaus +.. _@znicholls: https://github.com/znicholls .. _ESMValTool: https://github.com/ESMValGroup/ESMValTool .. _v75: https://cfconventions.org/Data/cf-standard-names/75/build/cf-standard-name-table.html .. _sphinx-panels: https://sphinx-panels.readthedocs.io/en/latest/ @@ -417,3 +476,8 @@ This document explains the changes made to Iris for this release .. _PyKE: https://pypi.org/project/scitools-pyke/ .. _matplotlib.rcdefaults: https://matplotlib.org/3.1.1/api/matplotlib_configuration_api.html?highlight=rcdefaults#matplotlib.rcdefaults .. _@owena11: https://github.com/owena11 +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose +.. _readthedocs: https://readthedocs.org/ +.. _CF Conventions and Metadata: https://cfconventions.org/ +.. _flake8: https://flake8.pycqa.org/en/stable/ +.. _nox: https://nox.thea.codes/en/stable/ diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 5c6e11f3acb..47ff6291b0a 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -9,7 +9,7 @@ References: -[CF] NetCDF Climate and Forecast (CF) Metadata conventions, Version 1.5, October, 2010. +[CF] NetCDF Climate and Forecast (CF) Metadata conventions. [NUG] NetCDF User's Guide, https://www.unidata.ucar.edu/software/netcdf/documentation/NUG/ """ diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index d0c3a3c5346..98f712a970e 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -8,8 +8,7 @@ See also: `netCDF4 python `_. -Also refer to document 'NetCDF Climate and Forecast (CF) Metadata Conventions', -Version 1.4, 27 February 2009. +Also refer to document 'NetCDF Climate and Forecast (CF) Metadata Conventions'. """ @@ -2490,7 +2489,7 @@ def save( """ Save cube(s) to a netCDF file, given the cube and the filename. - * Iris will write CF 1.5 compliant NetCDF files. + * Iris will write CF 1.7 compliant NetCDF files. * The attributes dictionaries on each cube in the saved cube list will be compared and common attributes saved as NetCDF global attributes where appropriate. diff --git a/lib/iris/quickplot.py b/lib/iris/quickplot.py index 42c0dba46ab..350d61b5372 100644 --- a/lib/iris/quickplot.py +++ b/lib/iris/quickplot.py @@ -49,7 +49,7 @@ def _title(cube_or_coord, with_units): if _use_symbol(units): units = units.symbol - if units.is_time_reference(): + elif units.is_time_reference(): # iris.plot uses matplotlib.dates.date2num, which is fixed to the below unit. if version.parse(_mpl_version) >= version.parse("3.3"): days_since = "1970-01-01" diff --git a/lib/iris/tests/idiff.py b/lib/iris/tests/idiff.py index e45d8a709ed..84a966624ff 100755 --- a/lib/iris/tests/idiff.py +++ b/lib/iris/tests/idiff.py @@ -220,7 +220,9 @@ def step_over_diffs(result_dir, action, display=True): count = len(results) for count_index, result_fname in enumerate(results): - key = os.path.splitext("-".join(result_fname.split("-")[1:]))[0] + key = os.path.splitext( + "-".join(result_fname.split("result-")[1:]) + )[0] try: # Calculate the test result perceptual image hash. phash = imagehash.phash( diff --git a/lib/iris/tests/results/analysis/sqrt.cml b/lib/iris/tests/results/analysis/sqrt.cml index 0dd0fe20b3a..c6b9b88e9a0 100644 --- a/lib/iris/tests/results/analysis/sqrt.cml +++ b/lib/iris/tests/results/analysis/sqrt.cml @@ -1,6 +1,6 @@ - + @@ -39,6 +39,6 @@ - + diff --git a/lib/iris/tests/results/imagerepo.json b/lib/iris/tests/results/imagerepo.json index f9430ae9f58..a7fd9e1faf7 100644 --- a/lib/iris/tests/results/imagerepo.json +++ b/lib/iris/tests/results/imagerepo.json @@ -908,6 +908,9 @@ "https://scitools.github.io/test-iris-imagehash/images/v4/bb433d4e94a4c6b9c15adaadc1fb6a469c8de43a3e07904e5f016b57984e1ea1.png", "https://scitools.github.io/test-iris-imagehash/images/v4/eea16affc05ab500956e974ac53f3d80925ac03f3f81c07e3fa12da1c27e3f80.png" ], + "iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol.0": [ + "https://scitools.github.io/test-iris-imagehash/images/v4/eea16affc05ab500956e974ac53f3d80925ac03f3f80c07e3fa12da1c27f3f80.png" + ], "iris.tests.test_quickplot.TestQuickplotCoordinatesGiven.test_non_cube_coordinate.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/fa816a85857a955ae17e957ec57e7a81855fc17e3a81c57e1a813a85c57a1a05.png", "https://scitools.github.io/test-iris-imagehash/images/v4/fe816a85857a957ac07f957ac07f3e80956ac07f3e80c07f3e813e85c07e3f80.png" diff --git a/lib/iris/tests/test_basic_maths.py b/lib/iris/tests/test_basic_maths.py index a559ee0e8a3..4b3cde95e4f 100644 --- a/lib/iris/tests/test_basic_maths.py +++ b/lib/iris/tests/test_basic_maths.py @@ -537,11 +537,12 @@ def test_multiplication_not_in_place(self): class TestExponentiate(tests.IrisTest): def setUp(self): self.cube = iris.tests.stock.global_pp() - self.cube.data = self.cube.data - 260 + # Increase dtype from float32 to float64 in order + # to avoid dtype quantization errors during maths. + self.cube.data = self.cube.data.astype(np.float64) - 260.0 def test_exponentiate(self): a = self.cube - a.data = a.data.astype(np.float64) e = pow(a, 4) self.assertCMLApproxData(e, ("analysis", "exponentiate.cml")) @@ -553,8 +554,8 @@ def test_square_root(self): e = a ** 0.5 - self.assertCML(e, ("analysis", "sqrt.cml")) self.assertArrayEqual(e.data, a.data ** 0.5) + self.assertCML(e, ("analysis", "sqrt.cml")) self.assertRaises(ValueError, iris.analysis.maths.exponentiate, a, 0.3) def test_type_error(self): diff --git a/lib/iris/tests/test_coding_standards.py b/lib/iris/tests/test_coding_standards.py index 00ce7b7d44a..79dff535eb8 100644 --- a/lib/iris/tests/test_coding_standards.py +++ b/lib/iris/tests/test_coding_standards.py @@ -102,6 +102,7 @@ def last_change_by_fname(): def test_license_headers(self): exclude_patterns = ( "setup.py", + "noxfile.py", "build/*", "dist/*", "docs/iris/gallery_code/*/*.py", diff --git a/lib/iris/tests/test_netcdf.py b/lib/iris/tests/test_netcdf.py index 75266ff3fe6..2d1b4a53d58 100644 --- a/lib/iris/tests/test_netcdf.py +++ b/lib/iris/tests/test_netcdf.py @@ -543,17 +543,20 @@ def test_noexist_directory(self): pass def test_bad_permissions(self): - # Non-exhaustive check that wrong permissions results in a suitable - # exception being raised. - dir_name = tempfile.mkdtemp() - fnme = os.path.join(dir_name, "tmp.nc") - try: - os.chmod(dir_name, stat.S_IREAD) - with self.assertRaises(IOError): - iris.fileformats.netcdf.Saver(fnme, "NETCDF4") - self.assertFalse(os.path.exists(fnme)) - finally: - os.rmdir(dir_name) + # Skip this test for the root user. This is applicable to + # running within a Docker container and/or CIaaS hosted testing. + if os.getuid(): + # Non-exhaustive check that wrong permissions results in a suitable + # exception being raised. + dir_name = tempfile.mkdtemp() + fname = os.path.join(dir_name, "tmp.nc") + try: + os.chmod(dir_name, stat.S_IREAD) + with self.assertRaises(PermissionError): + iris.fileformats.netcdf.Saver(fname, "NETCDF4") + self.assertFalse(os.path.exists(fname)) + finally: + shutil.rmtree(dir_name) @tests.skip_data diff --git a/lib/iris/tests/test_quickplot.py b/lib/iris/tests/test_quickplot.py index cf25324ea77..8abbf48a941 100644 --- a/lib/iris/tests/test_quickplot.py +++ b/lib/iris/tests/test_quickplot.py @@ -201,6 +201,13 @@ def test_pcolormesh(self): self.check_graphic() + def test_pcolormesh_str_symbol(self): + pcube = self._small().copy() + pcube.coords("level_height")[0].units = "centimeters" + qplt.pcolormesh(pcube) + + self.check_graphic() + def test_map(self): cube = self._slice(["grid_latitude", "grid_longitude"]) qplt.contour(cube) diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000000..cd97e8ef8b6 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,312 @@ +""" +Perform test automation with nox. + +For further details, see https://nox.thea.codes/en/stable/# + +""" + +import hashlib +import os +from pathlib import Path + +import nox + + +#: Default to reusing any pre-existing nox environments. +nox.options.reuse_existing_virtualenvs = True + +#: Name of the package to test. +PACKAGE = str("lib" / Path("iris")) + +#: Cirrus-CI environment variable hook. +PY_VER = os.environ.get("PY_VER", "3.7") + +#: Default cartopy cache directory. +CARTOPY_CACHE_DIR = os.environ.get("HOME") / Path(".local/share/cartopy") + + +def venv_cached(session): + """ + Determine whether the nox session environment has been cached. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Returns + ------- + bool + Whether the session has been cached. + + """ + result = False + yml = Path(f"requirements/ci/py{PY_VER.replace('.', '')}.yml") + tmp_dir = Path(session.create_tmp()) + cache = tmp_dir / yml.name + if cache.is_file(): + with open(yml, "rb") as fi: + expected = hashlib.sha256(fi.read()).hexdigest() + with open(cache, "r") as fi: + actual = fi.read() + result = actual == expected + return result + + +def cache_venv(session): + """ + Cache the nox session environment. + + This consists of saving a hexdigest (sha256) of the associated + conda requirements YAML file. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + """ + yml = Path(f"requirements/ci/py{PY_VER.replace('.', '')}.yml") + with open(yml, "rb") as fi: + hexdigest = hashlib.sha256(fi.read()).hexdigest() + tmp_dir = Path(session.create_tmp()) + cache = tmp_dir / yml.name + with open(cache, "w") as fo: + fo.write(hexdigest) + + +def cache_cartopy(session): + """ + Determine whether to cache the cartopy natural earth shapefiles. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + """ + if not CARTOPY_CACHE_DIR.is_dir(): + session.run( + "python", + "-c", + "import cartopy; cartopy.io.shapereader.natural_earth()", + ) + + +@nox.session +def flake8(session): + """ + Perform flake8 linting of iris. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + """ + # Pip install the session requirements. + session.install("flake8") + # Execute the flake8 linter on the package. + session.run("flake8", PACKAGE) + # Execute the flake8 linter on this file. + session.run("flake8", __file__) + + +@nox.session +def black(session): + """ + Perform black format checking of iris. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + """ + # Pip install the session requirements. + session.install("black==20.8b1") + # Execute the black format checker on the package. + session.run("black", "--check", PACKAGE) + # Execute the black format checker on this file. + session.run("black", "--check", __file__) + + +@nox.session(python=[PY_VER], venv_backend="conda") +def tests(session): + """ + Perform iris system, integration and unit tests. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Notes + ----- + See + - https://github.com/theacodes/nox/issues/346 + - https://github.com/theacodes/nox/issues/260 + + """ + if not venv_cached(session): + # Determine the conda requirements yaml file. + fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + # Back-door approach to force nox to use "conda env update". + command = ( + "conda", + "env", + "update", + f"--prefix={session.virtualenv.location}", + f"--file={fname}", + "--prune", + ) + session._run(*command, silent=True, external="error") + cache_venv(session) + + cache_cartopy(session) + session.run("python", "setup.py", "develop") + session.run( + "python", + "-m", + "iris.tests.runner", + "--default-tests", + "--system-tests", + ) + + +@nox.session(python=[PY_VER], venv_backend="conda") +def gallery(session): + """ + Perform iris gallery doc-tests. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Notes + ----- + See + - https://github.com/theacodes/nox/issues/346 + - https://github.com/theacodes/nox/issues/260 + + """ + if not venv_cached(session): + # Determine the conda requirements yaml file. + fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + # Back-door approach to force nox to use "conda env update". + command = ( + "conda", + "env", + "update", + f"--prefix={session.virtualenv.location}", + f"--file={fname}", + "--prune", + ) + session._run(*command, silent=True, external="error") + cache_venv(session) + + cache_cartopy(session) + session.run("python", "setup.py", "develop") + session.run( + "python", + "-m", + "iris.tests.runner", + "--gallery-tests", + ) + + +@nox.session(python=[PY_VER], venv_backend="conda") +def doctest(session): + """ + Perform iris doc-tests. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Notes + ----- + See + - https://github.com/theacodes/nox/issues/346 + - https://github.com/theacodes/nox/issues/260 + + """ + if not venv_cached(session): + # Determine the conda requirements yaml file. + fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + # Back-door approach to force nox to use "conda env update". + command = ( + "conda", + "env", + "update", + f"--prefix={session.virtualenv.location}", + f"--file={fname}", + "--prune", + ) + session._run(*command, silent=True, external="error") + cache_venv(session) + + cache_cartopy(session) + session.run("python", "setup.py", "develop") + session.cd("docs/iris") + session.run( + "make", + "clean", + "html", + external=True, + ) + session.run( + "make", + "doctest", + external=True, + ) + + +@nox.session(python=[PY_VER], venv_backend="conda") +def linkcheck(session): + """ + Perform iris doc link check. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Notes + ----- + See + - https://github.com/theacodes/nox/issues/346 + - https://github.com/theacodes/nox/issues/260 + + """ + if not venv_cached(session): + # Determine the conda requirements yaml file. + fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + # Back-door approach to force nox to use "conda env update". + command = ( + "conda", + "env", + "update", + f"--prefix={session.virtualenv.location}", + f"--file={fname}", + "--prune", + ) + session._run(*command, silent=True, external="error") + cache_venv(session) + + cache_cartopy(session) + session.run("python", "setup.py", "develop") + session.cd("docs/iris") + session.run( + "make", + "clean", + "html", + external=True, + ) + session.run( + "make", + "linkcheck", + external=True, + ) diff --git a/requirements/ci/py36.yml b/requirements/ci/py36.yml index 0461cdf880c..2b40fbad4e5 100644 --- a/requirements/ci/py36.yml +++ b/requirements/ci/py36.yml @@ -35,6 +35,7 @@ dependencies: - asv - black=20.8b1 - filelock + - flake8 - imagehash>=4.0 - nose - pillow<7 diff --git a/requirements/ci/py37.yml b/requirements/ci/py37.yml index 8817f575b7b..0f01f0ef755 100644 --- a/requirements/ci/py37.yml +++ b/requirements/ci/py37.yml @@ -35,6 +35,7 @@ dependencies: - asv - black=20.8b1 - filelock + - flake8 - imagehash>=4.0 - nose - pillow<7 diff --git a/requirements/setup.txt b/requirements/setup.txt index 2e14da49055..9232946a6aa 100644 --- a/requirements/setup.txt +++ b/requirements/setup.txt @@ -1,6 +1,4 @@ # Dependencies necessary to run setup.py of iris # ---------------------------------------------- -scitools-pyke setuptools>=40.8.0 -wheel diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a87902cbfda..00000000000 --- a/setup.cfg +++ /dev/null @@ -1,44 +0,0 @@ -[flake8] -ignore = E402,\ # Due to conditional imports - E226 # Due to whitespace around operators (e.g. 2*x + 3) -exclude = */iris/std_names.py,\ - */iris/fileformats/cf.py,\ - */iris/fileformats/dot.py,\ - */iris/fileformats/pp_load_rules.py,\ - */iris/fileformats/rules.py,\ - */iris/fileformats/um_cf_map.py,\ - */iris/fileformats/_pyke_rules/compiled_krb/*,\ - */iris/io/__init__.py,\ - */iris/io/format_picker.py,\ - */iris/tests/__init__.py,\ - */iris/tests/pp.py,\ - */iris/tests/system_test.py,\ - */iris/tests/test_analysis.py,\ - */iris/tests/test_analysis_calculus.py,\ - */iris/tests/test_basic_maths.py,\ - */iris/tests/test_cartography.py,\ - */iris/tests/test_cdm.py,\ - */iris/tests/test_cell.py,\ - */iris/tests/test_cf.py,\ - */iris/tests/test_constraints.py,\ - */iris/tests/test_coord_api.py,\ - */iris/tests/test_coord_categorisation.py,\ - */iris/tests/test_coordsystem.py,\ - */iris/tests/test_cube_to_pp.py,\ - */iris/tests/test_file_load.py,\ - */iris/tests/test_file_save.py,\ - */iris/tests/test_hybrid.py,\ - */iris/tests/test_intersect.py,\ - */iris/tests/test_io_init.py,\ - */iris/tests/test_iterate.py,\ - */iris/tests/test_load.py,\ - */iris/tests/test_merge.py,\ - */iris/tests/test_pp_cf.py,\ - */iris/tests/test_pp_module.py,\ - */iris/tests/test_pp_stash.py,\ - */iris/tests/test_pp_to_cube.py,\ - */iris/tests/test_quickplot.py,\ - */iris/tests/test_std_names.py,\ - */iris/tests/test_uri_callback.py,\ - */iris/tests/test_util.py - From 54b36fdc5bf52ea9065d9a3ea696a864c0f85937 Mon Sep 17 00:00:00 2001 From: Giacomo Caria <44147817+gcaria@users.noreply.github.com> Date: Mon, 7 Dec 2020 09:56:35 +0100 Subject: [PATCH 03/23] Add string arguments for Cube methods (#3931) * Test string argument for remove_ancilliary_variable * Enable string argument for cell_measure_dims and ancillary_variable_dims * Test string argument for cell_measure_dims and ancillary_variable_dims * Update with relevant entries --- docs/iris/src/whatsnew/latest.rst | 5 ++++- lib/iris/cube.py | 12 ++++++++---- lib/iris/tests/unit/cube/test_Cube.py | 26 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 67518e539a5..919b9ee6e66 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -22,7 +22,8 @@ This document explains the changes made to Iris for this release 🐛 Bugs Fixed ============= -* N/A +* `@gcaria`_ fixed :meth:`~iris.cube.Cube.cell_measure_dims` to also accept the string name of a :class:`~iris.coords.CellMeasure`. (:pull:`3931`) +* `@gcaria`_ fixed :meth:`~iris.cube.Cube.ancillary_variable_dims` to also accept the string name of a :class:`~iris.coords.AncillaryVariable`. (:pull:`3931`) 💣 Incompatible Changes @@ -53,3 +54,5 @@ This document explains the changes made to Iris for this release =========== * N/A + +.. _@gcaria: https://github.com/gcaria diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 3d0854355c0..bb631cae734 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1400,10 +1400,12 @@ def cell_measure_dims(self, cell_measure): Returns a tuple of the data dimensions relevant to the given CellMeasure. - * cell_measure - The CellMeasure to look for. + * cell_measure (string or CellMeasure) + The (name of the) cell measure to look for. """ + cell_measure = self.cell_measure(cell_measure) + # Search for existing cell measure (object) on the cube, faster lookup # than equality - makes no functional difference. matches = [ @@ -1422,10 +1424,12 @@ def ancillary_variable_dims(self, ancillary_variable): Returns a tuple of the data dimensions relevant to the given AncillaryVariable. - * ancillary_variable - The AncillaryVariable to look for. + * ancillary_variable (string or AncillaryVariable) + The (name of the) AncillaryVariable to look for. """ + ancillary_variable = self.ancillary_variable(ancillary_variable) + # Search for existing ancillary variable (object) on the cube, faster # lookup than equality - makes no functional difference. matches = [ diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 01dfe365b4f..63553ac821c 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -2044,6 +2044,14 @@ def test_remove_ancilliary_variable(self): ) self.assertEqual(self.cube._ancillary_variables_and_dims, []) + def test_remove_ancilliary_variable_by_name(self): + self.cube.remove_ancillary_variable("Quality of Detection") + self.assertEqual(self.cube._ancillary_variables_and_dims, []) + + def test_fail_remove_ancilliary_variable_by_name(self): + with self.assertRaises(AncillaryVariableNotFoundError): + self.cube.remove_ancillary_variable("notname") + class Test__getitem_CellMeasure(tests.IrisTest): def setUp(self): @@ -2146,6 +2154,16 @@ def test_fail_ancill_variable_dims(self): with self.assertRaises(AncillaryVariableNotFoundError): self.cube.ancillary_variable_dims(ancillary_variable) + def test_ancillary_variable_dims_by_name(self): + ancill_var_dims = self.cube.ancillary_variable_dims( + "number_of_observations" + ) + self.assertEqual(ancill_var_dims, (0, 1)) + + def test_fail_ancillary_variable_dims_by_name(self): + with self.assertRaises(AncillaryVariableNotFoundError): + self.cube.ancillary_variable_dims("notname") + class TestCellMeasures(tests.IrisTest): def setUp(self): @@ -2194,6 +2212,14 @@ def test_fail_cell_measure_dims(self): with self.assertRaises(CellMeasureNotFoundError): _ = self.cube.cell_measure_dims(a_cell_measure) + def test_cell_measure_dims_by_name(self): + cm_dims = self.cube.cell_measure_dims("area") + self.assertEqual(cm_dims, (0, 1)) + + def test_fail_cell_measure_dims_by_name(self): + with self.assertRaises(CellMeasureNotFoundError): + self.cube.cell_measure_dims("notname") + class Test_transpose(tests.IrisTest): def setUp(self): From 34834821a52e4742093b96f2737faae4dc68fa30 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Mon, 7 Dec 2020 11:55:37 +0000 Subject: [PATCH 04/23] Gallery: update seasonal ensemble example (#3933) * update seasonal example * add whatsnew * have matplotlib do contour choosing work * remove redundant import * review changes and grammar --- .../meteorology/plot_lagged_ensemble.py | 105 ++++++++---------- docs/iris/src/whatsnew/latest.rst | 4 +- 2 files changed, 50 insertions(+), 59 deletions(-) diff --git a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py index cb82a663d49..cdd39028c81 100644 --- a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py +++ b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py @@ -19,6 +19,7 @@ """ import matplotlib.pyplot as plt +import matplotlib.ticker import numpy as np import iris @@ -32,14 +33,11 @@ def realization_metadata(cube, field, fname): in the cube. """ - # add an ensemble member coordinate if one doesn't already exist + # Add an ensemble member coordinate if one doesn't already exist. if not cube.coords("realization"): - # the ensemble member is encoded in the filename as *_???.pp where ??? - # is the ensemble member + # The ensemble member is encoded in the filename as *_???.pp where ??? + # is the ensemble member. realization_number = fname[-6:-3] - - import iris.coords - realization_coord = iris.coords.AuxCoord( np.int32(realization_number), "realization", units="1" ) @@ -47,11 +45,16 @@ def realization_metadata(cube, field, fname): def main(): - # extract surface temperature cubes which have an ensemble member - # coordinate, adding appropriate lagged ensemble metadata + # Create a constraint to extract surface temperature cubes which have a + # "realization" coordinate. + constraint = iris.Constraint( + "surface_temperature", realization=lambda value: True + ) + # Use this to load our ensemble. The callback ensures all our members + # have the "realization" coordinate and therefore they will all be loaded. surface_temp = iris.load_cube( iris.sample_data_path("GloSea4", "ensemble_???.pp"), - iris.Constraint("surface_temperature", realization=lambda value: True), + constraint, callback=realization_metadata, ) @@ -59,18 +62,19 @@ def main(): # Plot #1: Ensemble postage stamps # ------------------------------------------------------------------------- - # for the purposes of this example, take the last time element of the cube - last_timestep = surface_temp[:, -1, :, :] + # For the purposes of this example, take the last time element of the cube. + # First get hold of the last time by slicing the coordinate. + last_time_coord = surface_temp.coord("time")[-1] + last_timestep = surface_temp.subset(last_time_coord) - # Make 50 evenly spaced levels which span the dataset - contour_levels = np.linspace( - np.min(last_timestep.data), np.max(last_timestep.data), 50 - ) + # Find the maximum and minimum across the dataset. + data_min = np.min(last_timestep.data) + data_max = np.max(last_timestep.data) - # Create a wider than normal figure to support our many plots + # Create a wider than normal figure to support our many plots. plt.figure(figsize=(12, 6), dpi=100) - # Also manually adjust the spacings which are used when creating subplots + # Also manually adjust the spacings which are used when creating subplots. plt.gcf().subplots_adjust( hspace=0.05, wspace=0.05, @@ -80,46 +84,42 @@ def main(): right=0.925, ) - # iterate over all possible latitude longitude slices + # Iterate over all possible latitude longitude slices. for cube in last_timestep.slices(["latitude", "longitude"]): - # get the ensemble member number from the ensemble coordinate + # Get the ensemble member number from the ensemble coordinate. ens_member = cube.coord("realization").points[0] - # plot the data in a 4x4 grid, with each plot's position in the grid - # being determined by ensemble member number the special case for the - # 13th ensemble member is to have the plot at the bottom right + # Plot the data in a 4x4 grid, with each plot's position in the grid + # being determined by ensemble member number. The special case for the + # 13th ensemble member is to have the plot at the bottom right. if ens_member == 13: plt.subplot(4, 4, 16) else: plt.subplot(4, 4, ens_member + 1) - cf = iplt.contourf(cube, contour_levels) + # Plot with 50 evenly spaced contour levels (49 intervals). + cf = iplt.contourf(cube, 49, vmin=data_min, vmax=data_max) - # add coastlines + # Add coastlines. plt.gca().coastlines() - # make an axes to put the shared colorbar in + # Make an axes to put the shared colorbar in. colorbar_axes = plt.gcf().add_axes([0.35, 0.1, 0.3, 0.05]) colorbar = plt.colorbar(cf, colorbar_axes, orientation="horizontal") - colorbar.set_label("%s" % last_timestep.units) - - # limit the colorbar to 8 tick marks - import matplotlib.ticker + colorbar.set_label(last_timestep.units) + # Limit the colorbar to 8 tick marks. colorbar.locator = matplotlib.ticker.MaxNLocator(8) colorbar.update_ticks() - # get the time for the entire plot - time_coord = last_timestep.coord("time") - time = time_coord.units.num2date(time_coord.bounds[0, 0]) + # Get the time for the entire plot. + time = last_time_coord.units.num2date(last_time_coord.bounds[0, 0]) - # set a global title for the postage stamps with the date formated by - # "monthname year" - plt.suptitle( - "Surface temperature ensemble forecasts for %s" - % (time.strftime("%B %Y"),) - ) + # Set a global title for the postage stamps with the date formated by + # "monthname year". + time_string = time.strftime("%B %Y") + plt.suptitle(f"Surface temperature ensemble forecasts for {time_string}") iplt.show() @@ -127,36 +127,25 @@ def main(): # Plot #2: ENSO plumes # ------------------------------------------------------------------------- - # Nino 3.4 lies between: 170W and 120W, 5N and 5S, so define a constraint - # which matches this - nino_3_4_constraint = iris.Constraint( - longitude=lambda v: -170 + 360 <= v <= -120 + 360, - latitude=lambda v: -5 <= v <= 5, + # Nino 3.4 lies between: 170W and 120W, 5N and 5S, so use the intersection + # method to restrict to this region. + nino_cube = surface_temp.intersection( + latitude=[-5, 5], longitude=[-170, -120] ) - nino_cube = surface_temp.extract(nino_3_4_constraint) - - # Subsetting a circular longitude coordinate always results in a circular - # coordinate, so set the coordinate to be non-circular - nino_cube.coord("longitude").circular = False - - # Calculate the horizontal mean for the nino region + # Calculate the horizontal mean for the nino region. mean = nino_cube.collapsed(["latitude", "longitude"], iris.analysis.MEAN) - # Calculate the ensemble mean of the horizontal mean. To do this, remove - # the "forecast_period" and "forecast_reference_time" coordinates which - # span both "relalization" and "time". - mean.remove_coord("forecast_reference_time") - mean.remove_coord("forecast_period") + # Calculate the ensemble mean of the horizontal mean. ensemble_mean = mean.collapsed("realization", iris.analysis.MEAN) - # take the ensemble mean from each ensemble member - mean -= ensemble_mean.data + # Take the ensemble mean from each ensemble member. + mean -= ensemble_mean plt.figure() for ensemble_member in mean.slices(["time"]): - # draw each ensemble member as a dashed line in black + # Draw each ensemble member as a dashed line in black. iplt.plot(ensemble_member, "--k") plt.title("Mean temperature anomaly for ENSO 3.4 region") diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 919b9ee6e66..e9fb007aca4 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -47,7 +47,8 @@ This document explains the changes made to Iris for this release 📚 Documentation ================ -* N/A +* `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. + (:pull:`3933`) 💼 Internal @@ -56,3 +57,4 @@ This document explains the changes made to Iris for this release * N/A .. _@gcaria: https://github.com/gcaria +.. _@rcomer: https://github.com/rcomer From 2c97fb584b785131c119e680b6257d793a02c04f Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Tue, 8 Dec 2020 17:47:53 +0000 Subject: [PATCH 05/23] remove stock_mdi_arrays.npz (#3913) Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> --- docs/iris/src/whatsnew/latest.rst | 3 ++- lib/iris/tests/stock_mdi_arrays.npz | Bin 18370 -> 0 bytes 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 lib/iris/tests/stock_mdi_arrays.npz diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index e9fb007aca4..44f54314227 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -54,7 +54,8 @@ This document explains the changes made to Iris for this release 💼 Internal =========== -* N/A +* `@rcomer`_ removed an old unused test file. (:pull:`3913`) + .. _@gcaria: https://github.com/gcaria .. _@rcomer: https://github.com/rcomer diff --git a/lib/iris/tests/stock_mdi_arrays.npz b/lib/iris/tests/stock_mdi_arrays.npz deleted file mode 100644 index 668c6d8473bcf46026debfbb826a5d3b33408332..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18370 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 Date: Wed, 16 Dec 2020 13:34:17 +0000 Subject: [PATCH 06/23] Put cube data on the x axis if plotting just a cube against a vertical or y coordinate (#3906) * Set 1D plots to put the coordinate on the y-axis where appropriate. * New target image-hashes for 1D y-axis-coordinate plots. * Whatsnew entry for 1D y-axis-coordinate plots. * Remove duplicate comment about flipping plot axes. * Use simple_1d stock cube for TestAxisLabels instead of simple_2d. * Only attempt 1D vertical axis swap if coord can be identified. * Simplify usages of plot() to take advantage of auto 1D vertical axis swap. --- .../oceanography/plot_atlantic_profiles.py | 7 ++---- .../regridding_plots/interpolate_column.py | 13 +++++----- docs/iris/src/whatsnew/latest.rst | 9 ++++++- lib/iris/plot.py | 13 ++++++++++ lib/iris/quickplot.py | 8 ++---- lib/iris/tests/results/imagerepo.json | 24 +++++------------- lib/iris/tests/unit/quickplot/test_plot.py | 25 +++++++++++++++++++ 7 files changed, 63 insertions(+), 36 deletions(-) diff --git a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py index a7e82c34f51..89d99c80b46 100644 --- a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py +++ b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py @@ -39,9 +39,8 @@ def main(): theta_1000m = theta.extract(depth_cons & lon_cons & lat_cons) salinity_1000m = salinity.extract(depth_cons & lon_cons & lat_cons) - # Plot these profiles on the same set of axes. In each case we call plot - # with two arguments, the cube followed by the depth coordinate. Putting - # them in this order places the depth coordinate on the y-axis. + # Plot these profiles on the same set of axes. Depth is automatically + # recognised as a vertical coordinate and placed on the y-axis. # The first plot is in the default axes. We'll use the same color for the # curve and its axes/tick labels. plt.figure(figsize=(5, 6)) @@ -49,7 +48,6 @@ def main(): ax1 = plt.gca() iplt.plot( theta_1000m, - theta_1000m.coord("depth"), linewidth=2, color=temperature_color, alpha=0.75, @@ -65,7 +63,6 @@ def main(): ax2 = plt.gca().twiny() iplt.plot( salinity_1000m, - salinity_1000m.coord("depth"), linewidth=2, color=salinity_color, alpha=0.75, diff --git a/docs/iris/src/userguide/regridding_plots/interpolate_column.py b/docs/iris/src/userguide/regridding_plots/interpolate_column.py index 273ef365ccb..4378ec98be1 100644 --- a/docs/iris/src/userguide/regridding_plots/interpolate_column.py +++ b/docs/iris/src/userguide/regridding_plots/interpolate_column.py @@ -1,4 +1,3 @@ -import iris import iris.quickplot as qplt import iris.analysis import matplotlib.pyplot as plt @@ -12,8 +11,13 @@ # Interpolate the "perfect" linear interpolation. Really this is just # a high number of interpolation points, in this case 1000 of them. -altitude_points = [("altitude", np.linspace(400, 1250, 1000))] -scheme = iris.analysis.Linear(extrapolation_mode="mask") +altitude_points = [ + ( + "altitude", + np.linspace(min(alt_coord.points), max(alt_coord.points), 1000), + ) +] +scheme = iris.analysis.Linear() linear_column = column.interpolate(altitude_points, scheme) # Now interpolate the data onto 10 evenly spaced altitude levels, @@ -27,7 +31,6 @@ # Plot the black markers for the original data. qplt.plot( column, - column.coord("altitude"), marker="o", color="black", linestyle="", @@ -39,7 +42,6 @@ # Plot the gray line to display the linear interpolation. qplt.plot( linear_column, - linear_column.coord("altitude"), color="gray", label="Linear interpolation", zorder=0, @@ -48,7 +50,6 @@ # Plot the red markers for the new data. qplt.plot( new_column, - new_column.coord("altitude"), marker="D", color="red", linestyle="", diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 44f54314227..302cab7817a 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -16,7 +16,11 @@ This document explains the changes made to Iris for this release ✨ Features =========== -* N/A +* `@pelson`_ and `@trexfeathers`_ enhanced :meth:iris.plot.plot and + :meth:iris.quickplot.plot to automatically place the cube on the x axis if + the primary coordinate being plotted against is a vertical coordinate. E.g. + ``iris.plot.plot(z_cube)`` will produce a z-vs-phenomenon plot, where before + it would have produced a phenomenon-vs-z plot. (:pull:`3906`) 🐛 Bugs Fixed @@ -57,5 +61,8 @@ This document explains the changes made to Iris for this release * `@rcomer`_ removed an old unused test file. (:pull:`3913`) + +.. _@pelson: https://github.com/pelson +.. _@trexfeathers: https://github.com/trexfeathers .. _@gcaria: https://github.com/gcaria .. _@rcomer: https://github.com/rcomer diff --git a/lib/iris/plot.py b/lib/iris/plot.py index 349f1fea104..bda5274ccac 100644 --- a/lib/iris/plot.py +++ b/lib/iris/plot.py @@ -665,7 +665,20 @@ def _get_plot_objects(args): # single argument v_object = args[0] u_object = _u_object_from_v_object(v_object) + u, v = _uv_from_u_object_v_object(u_object, args[0]) + + # If a single cube argument, and the associated dimension coordinate + # is vertical-like, put the coordinate on the y axis, and the data o + # the x. + if ( + isinstance(v_object, iris.cube.Cube) + and isinstance(u_object, iris.coords.Coord) + and iris.util.guess_coord_axis(u_object) in ["Y", "Z"] + ): + u_object, v_object = v_object, u_object + u, v = v, u + args = args[1:] return u_object, v_object, u, v, args diff --git a/lib/iris/quickplot.py b/lib/iris/quickplot.py index 350d61b5372..2eec514e9c6 100644 --- a/lib/iris/quickplot.py +++ b/lib/iris/quickplot.py @@ -138,12 +138,8 @@ def _get_titles(u_object, v_object): def _label_1d_plot(*args, **kwargs): - if len(args) > 1 and isinstance( - args[1], (iris.cube.Cube, iris.coords.Coord) - ): - xlabel, ylabel, title = _get_titles(*args[:2]) - else: - xlabel, ylabel, title = _get_titles(None, args[0]) + u_obj, v_obj, _, _, _ = iplt._get_plot_objects(args) + xlabel, ylabel, title = _get_titles(u_obj, v_obj) axes = kwargs.pop("axes", None) diff --git a/lib/iris/tests/results/imagerepo.json b/lib/iris/tests/results/imagerepo.json index a7fd9e1faf7..6c2bf66ba6a 100644 --- a/lib/iris/tests/results/imagerepo.json +++ b/lib/iris/tests/results/imagerepo.json @@ -288,9 +288,7 @@ "https://scitools.github.io/test-iris-imagehash/images/v4/8ff897066a01f0f2f818ee1eb007ca41853e3b81c57e36a991fe2ca9725e29ed.png" ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffac1547a0792546c179db7f1254f6d945b7392841678e895017e3e91c17a0f.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c1fa7a05b4ea6c059d2ff1494e4b90f26304846d78d1872a6cfc938b2e3e.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c1fa7a05b4fa6c059d2ef1494e4b90f26304847d78c1872a6cfc938b2e3e.png" + "https://scitools.github.io/test-iris-imagehash/images/v4/8fffc1dc7e019c70f001b70ee4386de1814e7938837b6a7f84d07c9f15b02f21.png" ], "iris.tests.test_plot.Test1dPlotMultiArgs.test_cube_coord.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7e0098757103a71ce4506dc3d11e7b20d2477ec094857db895217f6a.png", @@ -323,8 +321,7 @@ "https://scitools.github.io/test-iris-imagehash/images/v4/87ffb79e7f0060d8303fcd1eb007d801c52699e18d769e2199e60ce1da5629ed.png" ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_cube.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc1dc7e00b0dc66179d95f127cfc9d44959ba846658e891075a3e99415a2f.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffc1d87e00b49964179d28f16bce4b98724b268c6d58e1972e4874998b2e7e.png" + "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffc1de7e009c7030019786f438cde3810fd93c9b734a778ce47c9799b02731.png" ], "iris.tests.test_plot.Test1dQuickplotPlotMultiArgs.test_cube_coord.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/83fec1ff7f90987720029f1ef458cd43811cdb60d647de609485ddb899215f62.png", @@ -649,15 +646,10 @@ "https://scitools.github.io/test-iris-imagehash/images/v4/8bfe956b7c01c2f26300929dfc1e3c6690736f91817e3b0c84be6be5d1603ed1.png" ], "iris.tests.test_plot.TestPlot.test_y.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896266f068d873b83cb71e435725cd07c607ad07e70fcd0007a7881fe7ab8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896066f068d873b83cb71e435725cd07c607ad07c70fcd0007af881fe7bb8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8fe896366f0f8d93398bcb71e435f24ed074646ed07670acf010726d81f2798c.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/aff8946c7a14c99fb193d263e42432d8d00c2d27944a3f8dc5223ef703ff6b90.png" + "https://scitools.github.io/test-iris-imagehash/images/v4/8ff99c067e01e7166101c9c6b04396b5cd4e2f0993163de9c4fe7b79207e36a1.png" ], "iris.tests.test_plot.TestPlot.test_z.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/8ffac1547a0792546c179db7f1254f6d945b7392841678e895017e3e91c17a0f.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c1fa7a05b4ea6c059d2ff1494e4b90f26304846d78d1872a6cfc938b2e3e.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/8ff8c1fa7a05b4fa6c059d2ef1494e4b90f26304847d78c1872a6cfc938b2e3e.png" + "https://scitools.github.io/test-iris-imagehash/images/v4/8fffc1dc7e019c70f001b70ee4386de1814e7938837b6a7f84d07c9f15b02f21.png" ], "iris.tests.test_plot.TestPlotCitation.test.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/abf895067a1d9506f811783585437abd85426ab995067af9f00687f96afe87c8.png", @@ -836,14 +828,10 @@ "https://scitools.github.io/test-iris-imagehash/images/v4/82ff950b7f81c0d6620199bcfc5e986695734da1816e1b2c85be2b65d96276d1.png" ], "iris.tests.test_plot.TestQuickplotPlot.test_y.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/a7ffb6067f008d87339bc973e435d86ef034c87ad07c586cd001da69897e5838.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a7ffb6067f008d87339bc973e435d86ef034c87ad07cd86cd001da68897e58a8.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a7efb6367f008d97338fc973e435d86ef030c86ed070d86cd030d86d89f0d82c.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a2fbb46e7f10c99f2013d863e46498dcd06c0d2798421fa5dd221e7789ff6f10.png" + "https://scitools.github.io/test-iris-imagehash/images/v4/a3f9bc067e01c6166009c9c6b5439ee5cd4e0d2993361de9ccf65b79887636a9.png" ], "iris.tests.test_plot.TestQuickplotPlot.test_z.0": [ - "https://scitools.github.io/test-iris-imagehash/images/v4/83ffc1dc7e00b0dc66179d95f127cfc9d44959ba846658e891075a3e99415a2f.png", - "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffc1d87e00b49964179d28f16bce4b98724b268c6d58e1972e4874998b2e7e.png" + "https://scitools.github.io/test-iris-imagehash/images/v4/a3ffc1de7e009c7030019786f438cde3810fd93c9b734a778ce47c9799b02731.png" ], "iris.tests.test_plot.TestSimple.test_bounds.0": [ "https://scitools.github.io/test-iris-imagehash/images/v4/ea856a85954a957ac17e954ac17a9c3e956ac07e3e80c07f3e857aa5c27d3f80.png" diff --git a/lib/iris/tests/unit/quickplot/test_plot.py b/lib/iris/tests/unit/quickplot/test_plot.py index 0a88107a6fa..9bc4a7dca3c 100644 --- a/lib/iris/tests/unit/quickplot/test_plot.py +++ b/lib/iris/tests/unit/quickplot/test_plot.py @@ -8,6 +8,7 @@ # Import iris.tests first so that some things can be initialised before # importing anything else. import iris.tests as tests +from iris.tests.stock import simple_1d from iris.tests.unit.plot import TestGraphicStringCoord if tests.MPL_AVAILABLE: @@ -29,5 +30,29 @@ def test_xaxis_labels(self): self.assertBoundsTickLabels("xaxis") +class TestAxisLabels(tests.GraphicsTest): + def test_xy_cube(self): + c = simple_1d() + qplt.plot(c) + ax = qplt.plt.gca() + x = ax.xaxis.get_label().get_text() + self.assertEqual(x, "Foo") + y = ax.yaxis.get_label().get_text() + self.assertEqual(y, "Thingness") + + def test_yx_cube(self): + c = simple_1d() + c.transpose() + # Making the cube a vertical coordinate should change the default + # orientation of the plot. + c.coord("foo").attributes["positive"] = "up" + qplt.plot(c) + ax = qplt.plt.gca() + x = ax.xaxis.get_label().get_text() + self.assertEqual(x, "Thingness") + y = ax.yaxis.get_label().get_text() + self.assertEqual(y, "Foo") + + if __name__ == "__main__": tests.main() From e7eced5f2abea2867c1baac5e8ef30cd01d8ca19 Mon Sep 17 00:00:00 2001 From: "Max H. Balsmeier" Date: Wed, 27 Jan 2021 11:26:41 +0100 Subject: [PATCH 07/23] Extended the installation description (#3958) * Extended the installation description As mentioned in Issue 3949, installation of Iris on Ubuntu was very difficult. I described an easy way to install Iris on Ubuntu without conda here. * Renamed a label. _installing_from_source_with_conda -> _installing_from_source because tests failed * Formatting of code improved. * update links (#3942) * update links * added s to http * Reset. * Simplified installation and modified latest.rst. * Included missing link in latest.rst. Co-authored-by: Ruth Comer --- docs/iris/src/installing.rst | 38 ++++++++++++++++++++++++-- docs/iris/src/userguide/citation.rst | 2 +- docs/iris/src/userguide/cube_maths.rst | 2 +- docs/iris/src/whatsnew/latest.rst | 3 ++ 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/docs/iris/src/installing.rst b/docs/iris/src/installing.rst index 762fe60e4d4..5a81e59c88c 100644 --- a/docs/iris/src/installing.rst +++ b/docs/iris/src/installing.rst @@ -41,11 +41,45 @@ need the Iris sample data. This can also be installed using conda:: Further documentation on using conda and the features it provides can be found at https://conda.io/en/latest/index.html. +.. _installing_from_source_without_conda: + +Installing from source without conda on Debian-based Linux distros (devs) +------------------------------------------------------------------------- + +Iris can also be installed without a conda environment. The instructions in +this section are valid for Debian-based Linux distributions (Debian, Ubuntu, +Kubuntu, etc.). + +Iris and its dependencies need some shared libraries in order to work properly. +These can be installed +with apt:: + + sudo apt-get install python3-pip python3-tk libudunits2-dev libproj-dev proj-bin libgeos-dev libcunit1-dev + +Consider executing:: + + sudo apt-get update + +before and after installation of Debian packages. + +The rest can be done with pip. Begin with numpy:: + + pip3 install numpy + +Finally, Iris and its Python dependencies can be installed with the following +command:: + + pip3 install setuptools cftime==1.2.1 cf-units scitools-pyke scitools-iris + +This procedure was tested on a Ubuntu 20.04 system on the +27th of January, 2021. +Be aware that through updates of the involved Debian and/or Python packages, +dependency conflicts might arise or the procedure might have to modified. .. _installing_from_source: -Installing from source (devs) ------------------------------ +Installing from source with conda (devs) +---------------------------------------- The latest Iris source release is available from https://github.com/SciTools/iris. diff --git a/docs/iris/src/userguide/citation.rst b/docs/iris/src/userguide/citation.rst index 56eab0a4eb1..f91bc670f08 100644 --- a/docs/iris/src/userguide/citation.rst +++ b/docs/iris/src/userguide/citation.rst @@ -48,7 +48,7 @@ For example:: Iris. Met Office. git@github.com:SciTools/iris.git 06-03-2013 -.. _How to cite and describe software: http://software.ac.uk/so-exactly-what-software-did-you-use +.. _How to cite and describe software: https://software.ac.uk/how-cite-software Reference: [Jackson]_. diff --git a/docs/iris/src/userguide/cube_maths.rst b/docs/iris/src/userguide/cube_maths.rst index eebff53e624..1b1b2dbe662 100644 --- a/docs/iris/src/userguide/cube_maths.rst +++ b/docs/iris/src/userguide/cube_maths.rst @@ -243,7 +243,7 @@ unit (if ``a`` had units ``'m2'`` then ``a ** 0.5`` would result in a cube with units ``'m'``). Iris inherits units from `cf_units `_ -which in turn inherits from `UDUNITS `_. +which in turn inherits from `UDUNITS `_. As well as the units UDUNITS provides, cf units also provides the units ``'no-unit'`` and ``'unknown'``. A unit of ``'no-unit'`` means that the associated data is not suitable for describing with a unit, cf units diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 302cab7817a..6205dc6bfaf 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -53,6 +53,8 @@ This document explains the changes made to Iris for this release * `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. (:pull:`3933`) +* `@MHBalsmeier`_ Described non-conda installation on Debian-based distros. + (:pull:`3958`) 💼 Internal @@ -66,3 +68,4 @@ This document explains the changes made to Iris for this release .. _@trexfeathers: https://github.com/trexfeathers .. _@gcaria: https://github.com/gcaria .. _@rcomer: https://github.com/rcomer +.. _@MHBalsmeier: https://github.com/MHBalsmeier From 02777db967fc1dba1aa47611af1705ddb0e5cdb4 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Wed, 27 Jan 2021 11:11:20 +0000 Subject: [PATCH 08/23] Merge back v3p0p0 (#3960) * Add release highlights and pin rc version (#3898) * Add release highlights and pin rc version * review actions * reorder release highlights (#3899) Tweak release highlights * Add whatsnew announcement (#3900) * Fix spelling (#3903) * Fix unit label handling (#3902) * Add failing test of plotting * Implement fix to pass test * Update idiff to ignore irrelevant hyphens in path * Update imagerepo (following docs) * Update after review by @trexfeathers * Add whatsnew entries * Move whatsnew entries into correct file * Release Docs Improvements (#3895) * Minor phrasing change in 'Release candidate'. * Before release deprecations. * Whatsnew highlights section. * Relax setup.py setup requirements (#3909) * Updated CF saver version in User Guide and docstring (#3925) * Updated CF saver version in User Guide and docstring * Remove references to CF version of the loader in docstrings * Added whatsnew * Pin cftime<1.3.0 * Migrate to cirrus-ci (#3928) * migrate from travis-ci to cirrus-ci * added whatsnew entries * ignore url for doc link check (#3929) * whatsnew for coord default units (#3924) * Cube._summary_coord_extra: efficiency and bugfix (#3922) * Add Documentation Title Case Capitalization (#3940) * Use Title Case Capitalisation for Documentation * add whatsnew enter * CI requirements drop pip packages (#3939) * requirements pip to conda * use pip install over develop * default PY_VER to python versions * update links (#3942) * update links * added s to http * Add support for 1-d weights in collapse. (#3943) * Remove warning for convert_units on lazy data (#3951) * drop stickler references in docs (#3953) * drop stickler references in docs * remove sticker from common links * update docs for travis-ci to cirrus-ci (#3954) * update docs for travis-ci to cirrus-ci * add 'travis-ci' reference locally to whatsnew * update whatsnew comment * docs for nox (#3955) * docs for nox * add titles, notices and additional detail * review actions * Resolve test coverage (#3947) * test coverage for __init__ and __call__ * test coverage for metadata resolve and coverage * partial test coverage for metadata mapping * python 3.6 workaround for deepcopy of mock.sentinel * test coverage for Resolve._free_mapping * test coverage for Resolve convenience methods * add test stub for Resolve._metadata_mapping * fix Test__tgt_cube_position * test coverage for shape * test coverage for _as_compatible_cubes * test coverage for Resolve._metadata_mapping * test coverage for Resolve._prepare_common_dim_payload * test coverage for Resolve._prepare_common_aux_payload * test coverage for Resolve._prepare_points_and_bounds * test coverage for Resolve._create_prepared_item * test coverage for Resolve._prepare_local_payload_dim * test coverage for Resolve._prepare_local_payload_aux * test coverage for Resolve._prepare_local_payload_scalar + docs URL skip * test coverage for Resolve._prepare_local_payload * test coverage for Resolve._metadata_prepare * added docs URL linkcheck skip * test coverage for Resolve._prepare_factory_payload * test coverage for Resolve._get_prepared_item * review actions * test coverage for Resolve.cube * pin v3.0.0 version and whatnew date (#3956) * update github ci checks image (#3957) Co-authored-by: tkknight <2108488+tkknight@users.noreply.github.com> Co-authored-by: Zeb Nicholls Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Co-authored-by: Jon Seddon <17068361+jonseddon@users.noreply.github.com> Co-authored-by: Ruth Comer Co-authored-by: Patrick Peglar --- .../general/plot_SOI_filtering.py | 2 +- .../general/plot_anomaly_log_colouring.py | 2 +- .../gallery_code/general/plot_coriolis.py | 2 +- .../general/plot_cross_section.py | 2 +- .../general/plot_custom_aggregation.py | 2 +- .../general/plot_custom_file_loading.py | 2 +- .../gallery_code/general/plot_global_map.py | 2 +- .../general/plot_lineplot_with_legend.py | 2 +- .../gallery_code/general/plot_polar_stereo.py | 2 +- .../general/plot_polynomial_fit.py | 2 +- .../plot_projections_and_annotations.py | 2 +- .../general/plot_rotated_pole_mapping.py | 2 +- .../gallery_code/meteorology/plot_COP_1d.py | 2 +- .../gallery_code/meteorology/plot_COP_maps.py | 2 +- .../iris/gallery_code/meteorology/plot_TEC.py | 2 +- .../meteorology/plot_hovmoller.py | 2 +- .../meteorology/plot_lagged_ensemble.py | 2 +- .../meteorology/plot_wind_speed.py | 4 +- .../oceanography/plot_atlantic_profiles.py | 2 +- .../oceanography/plot_load_nemo.py | 2 +- docs/iris/src/common_links.inc | 5 +- docs/iris/src/conf.py | 8 +- docs/iris/src/copyright.rst | 6 +- docs/iris/src/developers_guide/ci_checks.png | Bin 24457 -> 203990 bytes .../developers_guide/contributing_changes.rst | 2 +- .../contributing_ci_tests.rst | 27 +- .../contributing_code_formatting.rst | 2 +- .../contributing_codebase_index.rst | 2 +- .../contributing_deprecations.rst | 12 +- .../contributing_documentation.rst | 10 +- .../contributing_getting_involved.rst | 2 +- .../contributing_graphics_tests.rst | 14 +- .../contributing_pull_request_checklist.rst | 6 +- .../contributing_running_tests.rst | 98 +- .../developers_guide/contributing_testing.rst | 8 +- .../documenting/docstrings.rst | 10 +- .../documenting/rest_guide.rst | 4 +- .../documenting/whats_new_contributions.rst | 12 +- .../gitwash/configure_git.rst | 6 +- .../gitwash/development_workflow.rst | 28 +- .../src/developers_guide/gitwash/forking.rst | 6 +- .../src/developers_guide/gitwash/index.rst | 2 +- .../developers_guide/gitwash/set_up_fork.rst | 8 +- docs/iris/src/developers_guide/release.rst | 20 +- docs/iris/src/further_topics/index.rst | 2 +- .../iris/src/further_topics/lenient_maths.rst | 12 +- .../src/further_topics/lenient_metadata.rst | 16 +- docs/iris/src/further_topics/metadata.rst | 40 +- docs/iris/src/index.rst | 4 +- docs/iris/src/installing.rst | 10 +- .../iris/src/techpapers/change_management.rst | 14 +- docs/iris/src/techpapers/index.rst | 2 +- .../src/techpapers/missing_data_handling.rst | 4 +- docs/iris/src/techpapers/um_files_loading.rst | 14 +- docs/iris/src/userguide/citation.rst | 6 +- docs/iris/src/userguide/code_maintenance.rst | 6 +- docs/iris/src/userguide/cube_maths.rst | 10 +- docs/iris/src/userguide/cube_statistics.rst | 10 +- .../interpolation_and_regridding.rst | 12 +- docs/iris/src/userguide/iris_cubes.rst | 8 +- .../iris/src/userguide/loading_iris_cubes.rst | 12 +- docs/iris/src/userguide/merge_and_concat.rst | 8 +- docs/iris/src/userguide/navigating_a_cube.rst | 12 +- docs/iris/src/userguide/plotting_a_cube.rst | 34 +- .../iris/src/userguide/real_and_lazy_data.rst | 10 +- docs/iris/src/userguide/saving_iris_cubes.rst | 18 +- docs/iris/src/userguide/subsetting_a_cube.rst | 8 +- docs/iris/src/whatsnew/1.0.rst | 20 +- docs/iris/src/whatsnew/1.1.rst | 6 +- docs/iris/src/whatsnew/1.10.rst | 6 +- docs/iris/src/whatsnew/1.11.rst | 2 +- docs/iris/src/whatsnew/1.13.rst | 4 +- docs/iris/src/whatsnew/1.2.rst | 4 +- docs/iris/src/whatsnew/1.3.rst | 10 +- docs/iris/src/whatsnew/1.4.rst | 28 +- docs/iris/src/whatsnew/1.5.rst | 2 +- docs/iris/src/whatsnew/1.6.rst | 20 +- docs/iris/src/whatsnew/1.7.rst | 6 +- docs/iris/src/whatsnew/1.8.rst | 4 +- docs/iris/src/whatsnew/1.9.rst | 6 +- docs/iris/src/whatsnew/2.0.rst | 4 +- docs/iris/src/whatsnew/2.1.rst | 6 +- docs/iris/src/whatsnew/2.2.rst | 2 +- docs/iris/src/whatsnew/2.3.rst | 2 +- docs/iris/src/whatsnew/2.4.rst | 2 +- docs/iris/src/whatsnew/3.0.rst | 25 +- docs/iris/src/whatsnew/index.rst | 2 +- lib/iris/__init__.py | 2 +- lib/iris/common/resolve.py | 714 ++- lib/iris/cube.py | 36 +- .../tests/unit/common/resolve/__init__.py | 6 + .../tests/unit/common/resolve/test_Resolve.py | 4795 +++++++++++++++++ lib/iris/tests/unit/cube/test_Cube.py | 112 + noxfile.py | 30 +- requirements/ci/py36.yml | 7 +- requirements/ci/py37.yml | 6 +- 96 files changed, 6030 insertions(+), 451 deletions(-) mode change 100644 => 100755 docs/iris/src/developers_guide/ci_checks.png create mode 100644 lib/iris/tests/unit/common/resolve/__init__.py create mode 100644 lib/iris/tests/unit/common/resolve/test_Resolve.py diff --git a/docs/iris/gallery_code/general/plot_SOI_filtering.py b/docs/iris/gallery_code/general/plot_SOI_filtering.py index 116e819af7a..d7948ac9651 100644 --- a/docs/iris/gallery_code/general/plot_SOI_filtering.py +++ b/docs/iris/gallery_code/general/plot_SOI_filtering.py @@ -1,5 +1,5 @@ """ -Applying a filter to a time-series +Applying a Filter to a Time-Series ================================== This example demonstrates low pass filtering a time-series by applying a diff --git a/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py b/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py index b0cee818de5..778f92db1b8 100644 --- a/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py +++ b/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py @@ -1,5 +1,5 @@ """ -Colouring anomaly data with logarithmic scaling +Colouring Anomaly Data With Logarithmic Scaling =============================================== In this example, we need to plot anomaly data where the values have a diff --git a/docs/iris/gallery_code/general/plot_coriolis.py b/docs/iris/gallery_code/general/plot_coriolis.py index cc67d1267c3..77066d362af 100644 --- a/docs/iris/gallery_code/general/plot_coriolis.py +++ b/docs/iris/gallery_code/general/plot_coriolis.py @@ -1,5 +1,5 @@ """ -Deriving the Coriolis frequency over the globe +Deriving the Coriolis Frequency Over the Globe ============================================== This code computes the Coriolis frequency and stores it in a cube with diff --git a/docs/iris/gallery_code/general/plot_cross_section.py b/docs/iris/gallery_code/general/plot_cross_section.py index a4bc918fc7b..12f4bdb0dc4 100644 --- a/docs/iris/gallery_code/general/plot_cross_section.py +++ b/docs/iris/gallery_code/general/plot_cross_section.py @@ -1,5 +1,5 @@ """ -Cross section plots +Cross Section Plots =================== This example demonstrates contour plots of a cross-sectioned multi-dimensional diff --git a/docs/iris/gallery_code/general/plot_custom_aggregation.py b/docs/iris/gallery_code/general/plot_custom_aggregation.py index 9c847be7798..5fba3669b6e 100644 --- a/docs/iris/gallery_code/general/plot_custom_aggregation.py +++ b/docs/iris/gallery_code/general/plot_custom_aggregation.py @@ -1,5 +1,5 @@ """ -Calculating a custom statistic +Calculating a Custom Statistic ============================== This example shows how to define and use a custom diff --git a/docs/iris/gallery_code/general/plot_custom_file_loading.py b/docs/iris/gallery_code/general/plot_custom_file_loading.py index b96e152bf8b..6890650704a 100644 --- a/docs/iris/gallery_code/general/plot_custom_file_loading.py +++ b/docs/iris/gallery_code/general/plot_custom_file_loading.py @@ -1,5 +1,5 @@ """ -Loading a cube from a custom file format +Loading a Cube From a Custom File Format ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This example shows how a custom text file can be loaded using the standard Iris diff --git a/docs/iris/gallery_code/general/plot_global_map.py b/docs/iris/gallery_code/general/plot_global_map.py index 41fd2269217..8d2bdee174c 100644 --- a/docs/iris/gallery_code/general/plot_global_map.py +++ b/docs/iris/gallery_code/general/plot_global_map.py @@ -1,5 +1,5 @@ """ -Quickplot of a 2d cube on a map +Quickplot of a 2D Cube on a Map =============================== This example demonstrates a contour plot of global air temperature. The plot diff --git a/docs/iris/gallery_code/general/plot_lineplot_with_legend.py b/docs/iris/gallery_code/general/plot_lineplot_with_legend.py index 5641b9c4d00..78401817bab 100644 --- a/docs/iris/gallery_code/general/plot_lineplot_with_legend.py +++ b/docs/iris/gallery_code/general/plot_lineplot_with_legend.py @@ -1,5 +1,5 @@ """ -Multi-line temperature profile plot +Multi-Line Temperature Profile Plot ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ """ diff --git a/docs/iris/gallery_code/general/plot_polar_stereo.py b/docs/iris/gallery_code/general/plot_polar_stereo.py index bd4a11923d9..71c0f3b00ec 100644 --- a/docs/iris/gallery_code/general/plot_polar_stereo.py +++ b/docs/iris/gallery_code/general/plot_polar_stereo.py @@ -1,5 +1,5 @@ """ -Example of a polar stereographic plot +Example of a Polar Stereographic Plot ===================================== Demonstrates plotting data that are defined on a polar stereographic diff --git a/docs/iris/gallery_code/general/plot_polynomial_fit.py b/docs/iris/gallery_code/general/plot_polynomial_fit.py index 237f4044b64..5da5d50571b 100644 --- a/docs/iris/gallery_code/general/plot_polynomial_fit.py +++ b/docs/iris/gallery_code/general/plot_polynomial_fit.py @@ -1,5 +1,5 @@ """ -Fitting a polynomial +Fitting a Polynomial ==================== This example demonstrates computing a polynomial fit to 1D data from an Iris diff --git a/docs/iris/gallery_code/general/plot_projections_and_annotations.py b/docs/iris/gallery_code/general/plot_projections_and_annotations.py index e59bb236d7a..f93ac3714fa 100644 --- a/docs/iris/gallery_code/general/plot_projections_and_annotations.py +++ b/docs/iris/gallery_code/general/plot_projections_and_annotations.py @@ -1,5 +1,5 @@ """ -Plotting in different projections +Plotting in Different Projections ================================= This example shows how to overlay data and graphics in different projections, diff --git a/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py b/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py index 063fe93674a..8a0c80c7076 100644 --- a/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py +++ b/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py @@ -1,5 +1,5 @@ """ -Rotated pole mapping +Rotated Pole Mapping ===================== This example uses several visualisation methods to achieve an array of diff --git a/docs/iris/gallery_code/meteorology/plot_COP_1d.py b/docs/iris/gallery_code/meteorology/plot_COP_1d.py index 2f93627b77a..bebbad4224a 100644 --- a/docs/iris/gallery_code/meteorology/plot_COP_1d.py +++ b/docs/iris/gallery_code/meteorology/plot_COP_1d.py @@ -1,5 +1,5 @@ """ -Global average annual temperature plot +Global Average Annual Temperature Plot ====================================== Produces a time-series plot of North American temperature forecasts for 2 diff --git a/docs/iris/gallery_code/meteorology/plot_COP_maps.py b/docs/iris/gallery_code/meteorology/plot_COP_maps.py index a8e6055a775..5555a0b85c5 100644 --- a/docs/iris/gallery_code/meteorology/plot_COP_maps.py +++ b/docs/iris/gallery_code/meteorology/plot_COP_maps.py @@ -1,5 +1,5 @@ """ -Global average annual temperature maps +Global Average Annual Temperature Maps ====================================== Produces maps of global temperature forecasts from the A1B and E1 scenarios. diff --git a/docs/iris/gallery_code/meteorology/plot_TEC.py b/docs/iris/gallery_code/meteorology/plot_TEC.py index df2e29ef19c..71a743a1612 100644 --- a/docs/iris/gallery_code/meteorology/plot_TEC.py +++ b/docs/iris/gallery_code/meteorology/plot_TEC.py @@ -1,5 +1,5 @@ """ -Ionosphere space weather +Ionosphere Space Weather ======================== This space weather example plots a filled contour of rotated pole point diff --git a/docs/iris/gallery_code/meteorology/plot_hovmoller.py b/docs/iris/gallery_code/meteorology/plot_hovmoller.py index 9f18b8021e4..e9f8207a940 100644 --- a/docs/iris/gallery_code/meteorology/plot_hovmoller.py +++ b/docs/iris/gallery_code/meteorology/plot_hovmoller.py @@ -1,5 +1,5 @@ """ -Hovmoller diagram of monthly surface temperature +Hovmoller Diagram of Monthly Surface Temperature ================================================ This example demonstrates the creation of a Hovmoller diagram with fine control diff --git a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py index cdd39028c81..5cd2752f39b 100644 --- a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py +++ b/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py @@ -1,5 +1,5 @@ """ -Seasonal ensemble model plots +Seasonal Ensemble Model Plots ============================= This example demonstrates the loading of a lagged ensemble dataset from the diff --git a/docs/iris/gallery_code/meteorology/plot_wind_speed.py b/docs/iris/gallery_code/meteorology/plot_wind_speed.py index 6844d3874cc..79be64ddd74 100644 --- a/docs/iris/gallery_code/meteorology/plot_wind_speed.py +++ b/docs/iris/gallery_code/meteorology/plot_wind_speed.py @@ -1,6 +1,6 @@ """ -Plotting wind direction using quiver -=========================================================== +Plotting Wind Direction Using Quiver +==================================== This example demonstrates using quiver to plot wind speed contours and wind direction arrows from wind vector component input data. The vector components diff --git a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py index 89d99c80b46..dc038ecffe5 100644 --- a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py +++ b/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py @@ -1,5 +1,5 @@ """ -Oceanographic profiles and T-S diagrams +Oceanographic Profiles and T-S Diagrams ======================================= This example demonstrates how to plot vertical profiles of different diff --git a/docs/iris/gallery_code/oceanography/plot_load_nemo.py b/docs/iris/gallery_code/oceanography/plot_load_nemo.py index 5f2b72c956f..c7ad5aaee4a 100644 --- a/docs/iris/gallery_code/oceanography/plot_load_nemo.py +++ b/docs/iris/gallery_code/oceanography/plot_load_nemo.py @@ -1,5 +1,5 @@ """ -Load a time series of data from the NEMO model +Load a Time Series of Data From the NEMO Model ============================================== This example demonstrates how to load multiple files containing data output by diff --git a/docs/iris/src/common_links.inc b/docs/iris/src/common_links.inc index 0bc8ca60e61..3941bfaff23 100644 --- a/docs/iris/src/common_links.inc +++ b/docs/iris/src/common_links.inc @@ -12,9 +12,8 @@ .. _iris-sample-data: https://github.com/SciTools/iris-sample-data .. _test-iris-imagehash: https://github.com/SciTools/test-iris-imagehash .. _readthedocs.yml: https://github.com/SciTools/iris/blob/master/requirements/ci/readthedocs.yml -.. _travis-ci: https://travis-ci.org/github/SciTools/iris -.. _.travis.yml: https://github.com/SciTools/iris/blob/master/.travis.yml -.. _.stickler.yml: https://github.com/SciTools/iris/blob/master/.stickler.yml +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris +.. _.cirrus.yml: https://github.com/SciTools/iris/blob/master/.cirrus.yml .. _.flake8.yml: https://github.com/SciTools/iris/blob/master/.flake8 .. _GitHub Help Documentation: https://docs.github.com/en/github .. _using git: https://docs.github.com/en/github/using-git diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index ab7689479a9..7232d7c40ef 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -230,7 +230,7 @@ def autolog(message): "menu_links_name": "Support", "menu_links": [ ( - ' Source code', + ' Source Code', "https://github.com/SciTools/iris", ), ( @@ -242,11 +242,11 @@ def autolog(message): "https://groups.google.com/forum/#!forum/scitools-iris-dev", ), ( - ' StackOverflow for "How do I?"', + ' StackOverflow for "How Do I?"', "https://stackoverflow.com/questions/tagged/python-iris", ), ( - ' Legacy documentation', + ' Legacy Documentation', "https://scitools.org.uk/iris/docs/v2.4.0/index.html", ), ], @@ -269,6 +269,8 @@ def autolog(message): "http://schacon.github.com/git", "http://scitools.github.com/cartopy", "http://www.wmo.int/pages/prog/www/DPFS/documents/485_Vol_I_en_colour.pdf", + "https://software.ac.uk/how-cite-software", + "http://www.esrl.noaa.gov/psd/data/gridded/conventions/cdc_netcdf_standard.shtml", ] # list of sources to exclude from the build. diff --git a/docs/iris/src/copyright.rst b/docs/iris/src/copyright.rst index 08a40e5a1e9..16ac07acb36 100644 --- a/docs/iris/src/copyright.rst +++ b/docs/iris/src/copyright.rst @@ -1,8 +1,8 @@ -Iris copyright, licensing and contributors +Iris Copyright, Licensing and Contributors ========================================== -Iris code +Iris Code --------- All Iris source code, unless explicitly stated, is ``Copyright Iris @@ -20,7 +20,7 @@ You should find all source files with the following header: licensing details. -Iris documentation and examples +Iris Documentation and Examples ------------------------------- All documentation, examples and sample data found on this website and in source repository diff --git a/docs/iris/src/developers_guide/ci_checks.png b/docs/iris/src/developers_guide/ci_checks.png old mode 100644 new mode 100755 index cf93239dea4ef9ee6c265d5f74359993ca822e15..e088e03a6657cf5719468a850e4279c162723c58 GIT binary patch literal 203990 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

-F3$~3Eu?g0z()hSyh`PNw(fJxbD5Rcm-jaN2 zdwVrCYbD*OUXcA;qe=NMC0Xl;q>DW<|5`QpF)H=XeG)%!9Q@ben)cKMIa~Y|!r5UV zHw7xiN(=V+O3EnnkBfNl(^x)N5e=lkdlW%piE#Ev@Y9_s5zqYW?*`uajAQJ9SCB9f zw=IQa$$|b64}c%q+`Vx2Mm5?$KV7}Iv_7DZ59v!L0sJ+~K!6M-fR;?rI(~;GjlU;Hsl`p{&7$IOuiMu6Si${y z@`2B@!H6W7_h0qzEzru6jDjTz`C`hye_0H?{FcXZuu>a&R|{%27hI`e6*<{ki7Uhn(8&wr<;+o9^%T zn{Y0)m6I8t4khu<;=vtRbz5>Y76k-6HeUkE(lMIdIgebAVFWiUq6&sjse~N=*Wxr4 zX#}fIDC_vza_7^3>LI=^nlNpNOF5IZ&9V1Ln))L( zl);r#()96dkM=ojfcyFst!TPh&UDDyZ7%+8MzhOd{f=6`4xv^SYO%GIWAmg?Kl@NrM%%*LpEDhH3jjRh$SB?05qMdL+kUJPoeA;~4~Hc! z-$2qvwWt}v6|6~E`me%(9u;su>hzMoj>nNwI+KsgTnn5*@<~YX@Miu|`1PUQ>||f4 z7mrX*z4g&b=S2LK{zlBj-^EC<6-`r9g_35}do0)QU`wfG$g4s@bWGGz(3<9L1PmP0 zt3}E8mI3UPKIHYE?fV}uw8zyUOPt~P?4)ui2)By8ONe|H*`))xSJFWZOawag!~n)h z)-FkYT*~?cZ2ohfjh-nzrr?Ta>a{=XeMKM5DwP-|SG(C_qPGI1LM4xOvw$zr&=W84 zt$v&*rWmMPl7V^0mlPtkRIRzSnaQ^IV81dk!vL^B#`d!BXc^q?!!Ng2epcJ;23(Q4 zUIF}tWJmfYgihWY)KE85ea&N|<+|EEHN zZhl@i#czcrkxEViNek)EQgEJ)>fz5#gnQmHwVx9l5;4+swPnF5q8T~*UDX6gM`YN1 z%gWaa%Gt^>rAAwOrsF_TO7Spoh8R5MiPjGVlzD#_hh`D5)D9?tcT}VkB>q7#}@Ke<%Al2ElDlWHd(7+~SJQ zwD;wr&z1O7o9QGXT9K8miaPHLK4jv`56umGs_^I*RhfG+S*5MF8RPlz#Fq0F>JMtI zVdZ1tcv3dAH4$Ime3M%AMF*7M=y4Ug{b+F6Hr*ru*GNOX#a%z{$MBPvj3)7}Zl>C| zCET zf2aClf|--8+A2VkZ9*M6JioOWyQpU8^2NEWeLWOWEU+^KQ?;6>y8@x#2_UvDCh$$6{q!zMY@zpus#yzIvGXlDBpuLvNwn%5_vmMX#3TZjt7X7eT&OYA~l=S?1 z@$EE(>`u6oZnpa#^eT5nyc&M>Hu~7P1wq3_huHaDTIhXo^jhS&WZ?SM!>@Li>*t4Y zG(uppZ=5<+*+6XEv2&E8Mx{CJOr=F8$!Z^%i!wvfo|lYUKPB^KFu77K;GXLi!YaVw z`Lm}VCYKyjCEy=F-rb-t=Eo~79_u+lUL4IJhtox`xqu%+J;3(*$lh$MKsnW;=iZUk zLK74Bl?qyg`BGqqTkk0Hzc{>CmPWe-j_I%y3d@}3y1X> zX=DFjw=BLk5X*I?7RuGwxX~1A_uu|xF{;w}7{fQ&?T^>}zS^u$e4dY8p=Lb1-{aewxsWKkr);7>p(BH#aP zWP!D&JSYupd8+O!x7~=1yt-@;l;{J|>$-<{nirgZO2G6}Qt%DALbDE*LrY3HqC0*sngZM_s6 z?ke?KD~(^-J1OBIIC@}vEaZCWZh5F~KaLAw_wbfl@^5~#Gr8Uh3n_2jDpDI8kBOxh zTT&w~Cx#<5%;nBMT^dSL{przd3H4^cH%YFMr0eY{$5Z^m1%G)3Q!aK|zgUn|&o@y> zeXi6#O~q%Nw@Ps8Az(8zPFAG&<=abCQ)-ypgZB}&kA&?IwMq}4FDUEQIPI|S1y}4L z1)Z-ybW&0!6dWz zh+c?okWZ0zV~ceu*ysHEkI9>9^f}c*Zz8h zpoIA?>c0iH#t-4(Z=3dbr8kI{MzzMa{cKcA#M zL?zK&7)5aE9tG)bEna;(OTI;$?5~)4tJ*-OjwikwB%=a#RcugK?bM4ZUcw_{3!JGn zUW&E;)jL8dsC*7@gO&QT=JeohrEvC|kj_s=k2ZbB1PhuxEFiZUuaNN0Mr55q*=0SeS@rIncN#*zt@F{I&zbt)F1aPtbffXc{pjJ>co9BW0NQeKE?*G(z)V@x z-0gBA+Urk_@qDnd%D+x7;ga`U`W*P=)Cmwl%z}9_A{!RjMEAoz=@_6ud@VVz(TWs> zgwLn5PY<*ick?^jAG>8r?Fya0?rGArREPI8bOnGBj5+U{AKYS)0O1s^_RZ-jKEsKL zPBLOI&BRZ5mU~YX=~#k-00@lR@tba|Kl>W|`a)~6NX~i#sEJm1lydkj`l$H&@Nj`& z$0L+wP;ygv^WF651~JgR23=-+i@^vsHvUOs#&6X1lobJM5_X`!@HyE+dOycX&@j7L zu^uLmWL2N8ehreU%2zU#yxb*t^8PD_+3+P3l8Hvtttk#)UnTXuC@D8+*m5bi=aK#Jd6wRJd$m7P)x~q2qLo@iyjp)T5F|?4ezK;xgGbeK%l3Xdw z#_=MGdc-WtUTqP09jOOwaBlV(_&C~>ty*qJ2Bw#=XPCcC9aGBL5wPs4Vnm#-ZO>IV zlB5C!ML@hDC-ZSlOQz4w(_gl8fdFv6bH?KLXY9=cs+XYZswba!b<3H*M4^5a z-|Caku7~}5OwUL7&caENI=f1NS&F7aOPF7&R{}2;Uk#^=y-FDqB134 z3;BxK`xYJL8b~A!^kdP>=6i&~+-n!bo&~_N%17zeI4)ocN_sSl;|b%vU5TOMkM4wh z%^Ud?yC*B$J-6*T1u&`i;|Y8|pYjvQ&;o9JaVRK%0V9LE>QS8)QUy)7rnMHuXQR!w z)y$Dx{4(sbHx%y@UpCF`n@?pJ9rm^m$j)I*-VM#_t4H!EHz z`Nf4Ar9|ls!fc^Axh9*r*0W;O%gQOhg1t z-wSoA!@cqu%$Rs(w4jAXY0N0m*P&q*zicOy_C7gOy)Iq2Nh3Woq`rcPo?T(C=DgZ* zJ5d%qhFh-W(9y#`ToKhd3k5G;d5?%a5>~Q+{x0X5Wxc6{U&V?00W7{9bWcqp^o&zG zn=!)Ml06a`zqObjp;hYal^xS^eI_c@LL>cX6*Apm!tQIHa?(or#JJ^H=g9fKKe1=W z?NjPn2X71nOmZ=}jO|TB)y7petd)OJ$wa*C^&({O`~19?#3jp`+rh9mpzGc;J(9&nLb=i=pDUi1F#UdpNC zA%3$a2&T;pNvF|Vw$u5wFf~tAj_5~(7^-2!TWd>4?u-fx8r1t#k~cwH&0ec@N!>j# z^}y@MP_{rrKiyr1 znYki_vOj727#f&Jqw1%WlF$CbGRP&#&=ZW$t=3ig$fBqNd-|Y4DbdYdsZi^4TeQbu6yB00W=V3w-Ka#qKv~ z_W2=-`9k-M+BArIZq{p)mB7Xr9j(`RzHk$QX5ABg8;VHU&ZM9h^Fgq57zU#kR~<00 zKG_~mPL?>66S!j{E;q=Z(_JPRa8Scd;Lk1PGaY;>uj+xtJASc)8%Zmg za2qS~^v3?HrY4Z@+R}P=Ky2q#rMP_`b+p)viG4vQYef8uO6W{1q|psEY-A{I(O0$v zfcg}=w!YyG_s(&o%1nctT#){>La%Fcl^eZ#Mu=u!WPIzv908oW<)!Yem<(=9@P7De zXwkcS_hBkgI<9lK96$;fiUd#g?BdrC@<3rpAhmATgs=w5uKqahy929!cEuymjL)$b zS7!vToQQ|D4#S7#iuJU`%OTx5i6yk;+L8z(4UM4B1FzL~bB+-*Z}S;KGs8LQmtW2) zs27vUV+AL?#3WSGD_Su>p51&|&DouaY7)15{ctu0tpDfgmi z`p|y&hIH%QnvQSfON7|VO#eKoZ3UW{xY)l-7xN~!7S?$Yj(+R zRFf?;@^W8m?F-+KXqC}`V6Fc2<@2-egg*c28U3?N2et2*hyV8KS3Bz`ks1y6$(GKP zXWx~A#e4Z29=LMgZJ4#+3K$jv&J4g^o(=GwUY9EL8AS+~U~iA9GzeD}dS5OiLtIa) z?By1pN)e9Os%Z|k6vh<-56C#6^pR2%!z3o+PW(`9JswXvu{HQpQA|pKRTkQ{h8vf> zCn}RjnJGkK9vA^%F+-xEUFxeqV^VkyV#)PRA8h=<<$6#%1sH>=`rb%%W_Pd3%GMN{ z7*wxcpcK#f{Bi+~>mUrmde+Q$d5f(O+pSV!r0CFh)}WkP>}5*7y&(EIq2#^M-^TfR z?*1%#*OkQza8|z}(1<>T+SoFGkHKCk)oBc734mR8EZ){Z{E6LiqsTe*P3Wda43PRhsw~Xnq4}wCYW==YGjNGEEpv`*AP?Z012` zfS^p$r?Ykn%o^0(^30dRTn=yLQsmpIt&SKEZ-Z{EIN z#d5!)L%?Z>N!k_-@E>kCto`vlaPaDE|NiWF?)G}4hXybr5-T00Hd^w|*O4d-<>Vyk zy6^Fv=^oPEjRr6wV=Bv%Jnau9=_sUK?sxyNSP-|#Eany)ccAYQ#j*_=8ErdMil&o{ zZMo)cWS1pldp$E(eZJx)DmYqmi^2`v@0D2~$MSgdi0hJcAkKciLGE>d&u{z19NPj_ z*u9lTJmYD-OfWrA>X{814}|KXukFcm{!}7UylProFZXrnG|ILj>|5MAXWe%P9Roh0 znN~VyKd#p770`#&p=eKJGW{2<=78W$*;&SPjCJE#{HDa$?w%t*^cwwVNtU78H&z%P z+noe0il&cg#mpC_XuVF|uChJ0Mty!>;Tpqa+n3Od{}h;hej_3S#j$->fX*otvRMGV z)9m4K6d8pA@L&2Ksr)t`Ll@Fj2m?g8rJL%IlNkxI@BB7vOYB!rXn?#eLB+;Hc#XhUnJ2ue} z9Dl3TP)7|`gXPchB6`gH+K9xi*_OW9*?2x%DlY7*j-ns9fnPVTrl0m#!VgAc_u<4K zhG1Z+}iMK#vA$$L0Ddvdovpsud_HiR!dxG*to>dD0`F%GM)2i?lEKX6y#)JBNuV4qw! zCx`G(VT#0K5{+kj$TGY*mU2tb3sUao8Jw5tW??jEQK|g)B_vej6y{`KwM=Hm?65-! zV;MCr6Y}Uhpmvs+*jRSUacR*w~mLHZ~YBKD-Q)iqxH^8kT`p zjTmnlkq^zdEnPqN_IaSjOw|6#=c3xTD1;k`5Y%0{olMSNbq3TNI}x!;z7~Fqi^)CJ ztCF8yVkZ|e6x;4AU3e^SJVHZY{p}T#FkmQpA9|Dynu~MrzPp`las50wu_ydF3rZcT z%ML!c@$Lq49=GraiOLL4$ab1T6aFf81`8Xx zw28xYy0}+L4_gwSt;m^#ms3zt+%Qp-< zL@4&vtZVdDM%fZ}d>7mRhc6mlk`FRa-WGYjnTb$SYsf`|X5 za*WEzeg6BTq&a#L!he5t2!)}ATmJ0e9{)ydkw00&!I3&VxjOiLt?493pfJl#9)VaP z+ssEFRT@1TT--f@r{>t07y~=9>^&t=+d1HCmVti%RTc|qH&SZ*M(Q*wP}w>IC`ct; zT&DRFl11ZreE4{5#t=tZ$(*n0jyPSyJz=$5s2FH6TCI|RByT$QQ$gV{Vfe7kLt!;{ zjz5H0TRh31!$#4PH`1LilzHMxWL);Q2`!Gy(Jl{Wm*A|+rOjVbjJ<1ML@Mj?SATU! zht`{e7q7qoPrm{rqWcy9KGKW4e9F>8ZF-U6jHCZp} z$AySL70zBI4Yltp!_d!jf?n3!Hbi}Or4l`ZiZfzpi@m)ZZ&<2-I|J5Xe z_}NlG;*XQ7jDV0jP6x<9?F|$V!%~55CbR(@CrE^k;GA(?6KfBod#jP3GQWWl2-~df zJgpN%ci`}4~Xr_W6QW#%b=q33nxCN18^Tr??wn)R0E?s)w+c6{A(W4GRAWs)*Q!rk9mUKS4peSG~Bjh&!`Jc3JRLCOVa&RPP> zjaulTLO(@O2jZm?({Up}`fY@u)P+ZhyF_-h6ku9P%MkVLrY-b{{%c+T=zN6%F+1Gs z{H*Wc#H~}F!Zly_^9q?mYiRF{Fbv^ZUUWXwOHFa3E2Bf$NM5!NpACf_Xmz?#z+CWUI+$H9#?XDEc<>Jo}`O1=RNiRihq`T;ibW~(i7>-TVNSw9> z)-oOz$_Jc7~g;=FA^NPdvS8Yk3d9 zx>11R5@nHD?@Cx(3MG3;k4Ek=On0xDu{cbxZog{FW;8}y82FI!VZI>r={+P^luws~ z&eeNyquH3tZqI(5>dW&*=K0Uh#k$bKbkKF3z{q3`Q^GZ@qxl6V#I`>%j%?QO#N za+k>oOJEF|&zGiWB3kvk>lsC$L)pNePHqX8&AG51aKGhTPCoF87%9h;2|iq-YjPji zXUAp0IF)T^4r#h+%iD9Hz_}zv$I|(Ya)c~oeb1Wjl#gCJhY?L5$Rwz*=sVcIxcj`k zE`bT~24F=Ei$5}QWtll?cKDdm-E8~*sYvMlX1Y5fj56{kwf@D5>Gza{Is50DTt(%2=A36CD6UBD$f2dr zv-=W|_9Z%m@Z%LA4YopDx5l$@7Jy}r?u{7SCj+I{+KrgCmprr;4P2w7Ut$1LtA!X` zd9iq5zvH`49SgK{(^?&}XEULjyb5Y`3Fe*mPFTdmNsMK@S+(2k5;3Hw2{v%XNYJCq z!W4KE!XBa4T3Z#_bl-385w1Jr;Q@&>P329LbdA3Tr~SYo*Ssa`SIYx7l1XAVcl2T( zKX-O8Fu2l7&}7WjNG?7_2{z^4_@Q%}d|z`dBm$w6HHzmIzrIQLOQ#v<@3~w9rq>tBuRt2UVVN zNXOBA7b5AqB0YtLw@~PWkGl_(0kK!WzPy&)H~rQ-L;MpXFHAA`(1f0(+rbN*W7g%2 z7nis6*$CiSiPec-x6!@90_tOOwE194r;(`JZq`W4bugYPCvlb<(K}ie8Iib6{pQt= z+2`d~Z9W?23hs;sJUJBeQPnG?DyuU9^up7vATh-;`P&~Ls7`iCkr+?umA=hX$A}TrKy4i+) z2kwpUn@4j?45mwF9!pV9BX_(0INQMslFOsLdUxVVn*@hKCZv?b87_?=a#ax+xL&X* zHL0TY>_NkM2SNYjPVDU;q@^z7^S-|;3ZH6L~1j_>e zV1zHGAEkc(p zBxjl0vW=h^d(FOXoowwHhO7W4NGTb`F__^G#nu{Uc*2vz%*}9*$-sBvS{u~So4HB9 zVgGgi58Dof{qZRhhq*S%rE>y*rT>=)pyW0W@fZ<9q>_t_j@<%H5dN)N08w;IRuv~j zLKF)M4GUYwgEBu$@>}cG^=ptlGi%RM%KzGQf>!3g@KZk6#!^d&M~3V$g!YB=mk*|$ zDn8MweqwermFl;AJN{)s-J0eJmRBsZ#s0NQ5w3PWyY!<3n4r~sv=fqh35+pijlB?Q zOfG03?<000XeB30+N*D$fX#Hz=&z?)djK2L({3 zD-BumWxKD--0zGOP6ZPjW&|j5qjalRu(`V;2)h{Y$L+{r0;xP!_N_=Z2|}uUPDc*C z;n@(vUhD49O1C|guubhdS3ysnqbFWNizxr({`Z4T1ZoBjn%*^=?Lsx zDnWHqXVR7h=g`MWy*qKyeD$oy*=c7VKB7C$BflWMae?aO&?pwkbm>Pcr2}{qeaMQL zg0r`$#8@E~%_YKXyhZrp4+@R5)nXk}vJX!S-NQ{7SNurxHISrrOn-$T)aOP0G-?du z(OMzC(IT$7*4GBuLsH@Lk9wl{O_rzTuZii!+%DsH-r#l& z*!6Dma+n*v5+pirry$zbq977h!*g5KmZDzOi<+od=453gT}YNh4)5i)EfmYlyZwB} zq4r()sohxG;!|Ui8Tnhs&8d0I%;yQgSG?ypu)TnCe-S!ys~sQ&lC&k?y|8DJ0V4k2c^zn8#N>T47RrR$k8ZGEdhB&|uNg(|#@L|FS;+q{sVfPkj6ZmSpvu;UVY-D?PuG z#Sm?S$^D#qA3ut#g3&1wWIGnbombV zG4!No#)V?Hv-?lT-~g(a?3Jw+l(bDojoBND4`Awm)~Is_a+~U4Vu;Iv87U&U!~yAD zMhxWnIZh;fCwt|O$xlqB0-isly=p7*irStM<+JSLSV2nX|3fZKx=5rbGpm%X>~x+P%5CjA=aToI8bcELc+lg(~$kJd3D?q=al=Xx)H>4qU29! zeuX?LSn}G=52wXZJZ@dNWLGWArFrddac+@<62O_!EWz#hqRE{^Ja&I^r!hZBy12BB z8$9*b>snp(rc^=_+lgKm2LlL+S(k>MAlVVH&=I`$$zqNATP2poA1bL~Jx`B-1_eR6 z>2z06I(>IId|SoGJEP9QHB5GYe?Y3rcwA#Gk$UGFpQ!nrx06oj68hP1M z;@Apd0MCdNtf8c(G+j8zIMh4XI;R$Y1oZ73bGH_-|F)~!V?e+b!hplqwp*1oc6Wlo zf!042E{|XTFyR9Nm%aQ(PQ18tAlSv{qZh7DHfkc8SSe7Zmn5cLsKI2UE$u|Au<1m5 zgwf@Cg^l0S`{lqE1{uYh0CkrU6?@mpoNO{8P6nuPs}x=jUKg-d12Gyef+fk&+GTEg z_Y{&Ds|U`-K!8}W#f8^xO7$u1*WT=Fb9Jot-u?0*yujzoWiOGLZH$~M(?4H*UA#Fb zA&@&bXv2iPQ`lMz*>xnTySYMZeAS$qK&7K4@^X7rHNJeP_IPF-97$O$Tg)e5?@v~K z-6o+R?D)X-@|pV_Q)e6CL~V-P2b>6_jmZnhb!&c$kqtc7-Sse^a8k_@gx=>Y+`}Rm)=Os0ATsK1`oMyAVD=hhAIC=3$)XHx=>A+mOt< z#j?lbH=(?lYVkibzbsalVR~UlQ#=iJLJp0L+|`=X2w0DWm&{ig*U%pUB7ELa?K);RI5Lv*ACbGE>>h#}16-?SMW;W0C-MN}zrCqRSajXuYZ(~WJ=6<_ z+c~nvp3rvpw+Ec1|LSc*%JzDt!9r06fG)UHPs{I!1+?hX7OVdnmlhji;XbZt0W!dl z9S`cxRFg#MMKVjgQ$-J*Fl^t__6t>rz~>|&5z(wHX)rLLP1Fa&lD@hV^~{h%ln|zj zYnf1{$8+2qFNHf0zN-OiBt)tTsngFrV=7q~Jk;SC@ps1wnb1&LymFT81LW$6Q27z= z^@Rjs2FrW|jqfW$tB~`V)mP}udxz_H0()$pkAuaRVHfuRgjxGA-%GzM(Lj12)&nn! z+hgV&%U2uk6@4WA+Ca2}>yZZ3pbnOeCJ8T$D*Y#`^Dn9-?*WMaTE|Zv-MfAay5=pD zeHYkB&!%@b=Mu6%ZW~@Oc7@a!p2`=dxz19gTi`!f`$=MMeC79c%xHmMZ9k%o00Z#iHsyi)B5C`8PlV09pEU z0N;7DnDg`f}NR^F6w{w8|85kIQ{iifW_lYg#8ZzGj167o$yK|Pb8{|T^7$f`-?gvtw zTlIK*e+ELmO66~%PA|a}zh!u;XowjZz=HG7dduJbAF^!v-vVl~mf$G*M?_;s+D0O{U7C2I#=mt?5l=Mlh|9%#gV?!Uqt$|LER1!leQ zUaX-Qs;gP}|Nj%>E`av^WC!1s@jUDkONRG~IP3BX+q8Q(kB+Z7IT_WPR0-S6#jd{L z3^?QYtLrS-`NzCINY}VwG`&7_aIF6RUV&nC^}jq||F(kt69#+51lBKXJlUCKC|R;V z9|jazlxl-c;19D^L5+1)bV2%JuPksRfl!5^#g~mu6el48WrTwNwbFmT=Fj*3RdRYn z>f(`k3}9pxnPU6T4f&5{!VfDJE&fZ#eULg3{5krj0Z7v?>P4i1 zUATdQjhj;C&r{r4hc9-;u%iEk^c`aT`=3EzLx$CYxLjG~(7W*!h3+qVb;W&igc>Yz zeV@~~bcumIZabfQcsf7=J*Bf6L6lbBhs42R>zXUIA2;}no)vo|8(a>SJ$q6XsbkVw z9f|K~gQR26JvMnRWEKhk=f7Vu14g{{k#fI)ddng=AKKPC`Yq6^`N>HUg917-Sk65W zSq2#Q^?_8voj1TYdY5)G+$LRhx?$At(w)`|z$=~FP~1+n56P4)nf+_|DU}ZxYGfx@sGZB&Y8BNIE%Ca>e^Ww#w zUT>Yfv6?s)w~;Y^4vMTyEKzS~WMa*6VAH8|ivV;zd6l$WtRO|c+TCenU<<`uqk9Yx zHLN^g&gme1`;J1=k&jI9)c1?nw}+*3&&{6v$_szmr|o0D&8hrbe#IFfdFurctI86xA4Fy8|+_tdOrOA3c^s1W$0j4 zz9wWp|DZtzUH?4~1JsTMSk061fBhZF%P|!w^F<^I7?DS%3Cj_%{o~Vy_vZ0EI+oDo zzZ1M_Vj|EL7&E{A9Fw!D#(u7H*?U2K{&_Wai(=BEKP~UjI|5RV6{O?+~VC#V_`h6j^I!2OvK$O!HS#Np^B z=B|=#);M$o6ogAeJG9_CTeIh|@1b!MLC&qdkYiSa8s!aR26uNeuSkSKWX!_OWy9A% z9Q?{)D1lZI1pMYhMp8UarYH_*0v`Fx z9F~L4{m<-Yi}`^JONIYJbq^K(!5Qa~gLr?yk0#Hie;f;#j|ueu&J~0Iu7G)6nC+*< zjJw<Z_5&nyGpJ^s+X&;TMl(|HfhZi&^D%vz6DRWSV%rajeZILQY!s-lw>ca%+~d~ zHN(XkFw*g1iH;1A*qbdvNHtbAmMcudDPl2Z{B@!VhpbdBdB%O`RUwyt_UH9)SBl}b%E_JL-DjokK3Geob&X&{V@?f^QdTScnZLaR9c z=*n3Fn5&$le}g*~CCbZiwt75w5m794{8n>vvi5Vi*;*bODM<{n&~Rt?rm1x{VIEg$ zMsRN7?~F1W{EUTG-QWsu}Qi4-u(jnr`FD1wKj{%Q?~QO zN0Z8F5)nH-q8pf}yR(_$2^?~@``x3DCt4`HyohKE)ycjkHz}&0Hbg0T4hW;zoRNTY z__WxexMFfLGAOQaWad8YbEiykUxr#;K&dQp8WIusutFN%f~uz8cDZ@W0ZeTQ1Zc&f z0Ryiy%9n{H^96H35t6T=O%{A@5&@PWz}(NWbX!q0Fjq^Z5l%q{{~fu`SY_D9Cv^Wg z_4Be!J`=%^TASJAD|UO?R~t(J(CIOtRhn!5LnO(f2J6jJTR+|i!6Re{(qgKEtibnP z>dDVy*<^*u0vVwOVBS-Dy-WIx>$X(B(p};=oq!%ED0S_A-o_8*yHuYJVQa(fstH6nRlU~Qk)I*h9-Mck4xZ>7 zVmWs7CauM4R)r%pl0qmbC~th7y^@u+SGNizM?@t}_Ej?<74{l6?PcQ2}3)~UWuWZ})$ z!b_3ub+%}_Cz{`df7vgDGY_UI!!IFb4EdTtRFe%Fs?j7xT-Zj`H8!hNged~~eU%&I z2jwJ1FO$PLD(2h;?E)x5g`wEE(NrS{vOtb$Uh{FyXl$kQXPM!NB`??6-{$K#UyU7+ zSzC4$IdZ%ZuqJLLC)xE2fr*W(aRt(aMnGrOUgUqM2AZH(cC?IFCl<@O@{6|xHxC_B zk?e0jrvSfEXmB%Kvx7mPve^j4|5D8sPAtW)YLg<$TrTs-ierBsefd^5l2T)(sDT{J zk0oKQsM9WyCKGXQ4-X0b4Ok&R{3<6PD2C$_=95_qm2K8K-7uSYSy4&4a?whBj9)cq zaEt&50RUM0lFwk$Gm%GGsVyu+%RmvfWx06t>w`K#XcsAma0$%E#}wiqVBJcG6<<8= zb7@yEL%iT;*JeIk8^S2k_7louPhS-eseh5gz@hNx>OX4D zaRNjsvxCEWGWA5GrI4;s5As| zvq^I4HFT|L)#~4*L3bFyM%&~0>?)ysSKY2>2UNm?AB8YK{Sk578_w~g8HcL8?A7`X zH1}RJJZ??>ZRgY!OCrWl@-}xvv+)%{gYTlQ8)1w*n2`}2jNMfKb?UneaDx|YUS&LQ z^jUp-wy(S6A=7>g{+-CJEA;|HwGtT0DXR6h3}GvC58^D%0ppO`tas<`elOJ!N&rk* zfZEJ*j^mZ{YJJ!F=^34A-rRk*J3F%Q?l|GQ)2iQ&knNg8TB`?TNJ!^K`qeY;v`a6} z{%{S9!1$b>iq4r?_IBNK7VS0YQvF3NsBc6fmnyLN)U9_^KJer+s2n52hEd(B=RM|} zT?$jWk&iU;M{b6#+KHV|iuHtR74_q)W2^$fwI0~GkQHmp8FCZntp}*KaG(-#epmmb znzj2?Ow$!1l@iX7N2Ke?jri4B1x1bJAA7;J_LiVlshZZJG%H=6R8A`Yj*704cjN zfH|-llc;8YdiXSnXF?Q*a<$m_$7M-bR0S-&;~}^YQTe+hwMuSU@r|EZ-O~@h@SY8V z1`-%(a-uNSEYMJ!{mE707{?}D*fqArNaiZi`=F@Yo0CGsSeT4UC#~dt<$^bB>4(!9 zJI`L#ev(E9MbL9^BK=3ofJ{35ZOlC2le}iG5y98RCfX1n-DHjMYBIQR$#MB#b@fQZ z-^`zf3Tb5NKwZkH3H26CYw-TUvQ@e9$L!+)Q2_F@;qv&Gz?+@2wZH94I%1HJ-9ndi ze=G^|dNAE6n1@K2CK*eK^kh9;v>z{wAe0W8c1A-;yU0pL_wJwzD>#N` zXu*8hEiJIe97&gvMFC2{?`&b?HxI7DlR(phOZ$y?*+Q6_pi$Anr|4XODJ&QnaHfJ9 z-@ojP)8H%vB2L@?AA4^Z71y%1i$;*fo8WH2f;&Nj2W{M)V8Jy&BMA^(LU4BrL4#{> zcYkjF-Vq*d8b)#mt61ucg)t_N!4GH9mUV4~usSq`Q5UNn%oL{DN-y#9)4%Y^&+ z+2Cp=DQOYF$AYY$R(S4d)>`j2D<5%*e&G{3zstd|0A$?B_xhgKJJHWaGM9QiTl3>x!E5LX!}`NWsr?)6B6UXM*Yh|B z6?a0H?VaG;iXjSXTJl3g#P(abnFK;R(GQDCN{@$m#a4X%M(1Swq#L6qqHb%I1S4+* zCByom%}8RiOt-~LC<0RXr^;BmAM8=)mp?zFZ8JoBVh!on``a!|@-$`fr%cc@?a#_`D?C(KTxhgGhOAZs-V2i!qoz~{rg%b?l+vprA){GO#D9)C z;iNHzbFBBMwo*&=DUAvnU(+NIhC4oiaKx>{e-0Q_qhpY7dp<*5RSe2+dLsEUZvk-h zGrIjm+gd6JcP#sDdYy8y$SM;>wr+!s*Rl5d*IxCz3teJvQ*IucUqOjr39F)|Ssdfl zbVNNp9f0i|+*n^1@ig0^v^!Y|LOZ@paU3Jy>A4dUxd+h9}**xdM;2jd!Y<1p+g`zK2R`Ac2s z6-a~x;1&Q%137SVk$%%Y;GC>e)c&Fi*mIuc5&KjAHch4}vm6fKSN}SFG}{T7=a9Qu zm7DWoesVa+g?xM>9YHKL8_v2T<_?4!;3l5@!|{tVsIeHBl$YvI>;?jym5(=v3eTG?}k_1kWrT zQ4s>-%n78OBn1AHwog~bk)PrN-3-V9!wVkSNyKJ+1Bx9Rr$ABqI|$J5@09@6N?7)+trkrCz^=NiUSvmD;PZIr4*gUVZ7NsD^!_>Ah;j{p_)DxzA_TwRbjcC#VW&E-djsV*=sl#*^?0 zQgs;?lcnc3_*_sp6 zvt71Wcm{YGd#3|545$8D7t+!>Y8*W#dHWURs&BZQKy~s-{ zKBxm(Ln9Aq8+=BCprFXAQC!T5c77rOXKo`jFc!USH1`okU~ZIqe+4~gx#dIR-Yap@ zK&cHkmRBj|h*!v;9s-b)PrUr?d%7H(5C=OaEe3zMf8y8B7J3#i^UQ;hfLSL6upaAQ zH;097Qo;WjCDeap%M`NxSAS43fSv^eV_G_-vBvm#s7@#0jzFliEJlgXi58MlrL8ibY{8DYlRV>J~LDX2WIiT|4JVPk_01sAyBIC zmMy04c;d1;7a}1=P6O)LL!~D~hHNi2+px`;cc-k>w>a)P*0p8_e=|n0t~|vnR+_}O zPYt6aJO}WY9{CXPIo<{^enSYV64O`fR;G#d9_?uGJTzSM88qtvwVCG{=xi@odEN}9hVSc-%+J~SkNT2_v(h!pHis_ zWBL_vTj%qjOu)Q{o3%tz`+oHHYN^2tj)V&tqy%HOGcybU*J)48$PNI`QSDln%{j)+ z+m9zzwimB1m-X9*YZn6h)wEwOZwQ)`oCvrdK1hlwH@|=wftFSrfE3IkJ~e2$;X`I3 zIu@~re>tK=@0`y|;6&ff8z3fj-0Ydbt|S0IjDE>VIgw*WuavnSINO2;nA8FoO8{s8 zG-QB~K0G1#uqpV%vxzVODj%`>(zKuDQtW%>=-FMjv-bwiz|?Sbl%e#R4cO06vpHRz@su?dsHoTQ?Vx>;tkg7}~Ofyax#E$cI*Xi~aeU@KXZD-ZY*W9PC<$mG{3B@_^YbNOkP^!&Gg_g2mJCbqJ>UD~&?n zlsmRbvTX7+giC}H^lWyA;@R6#Bjm9^**cBFvZ)FUlFQYO43j6&b17UbZ2b-t;*S8= zfO1ug-)iF9^FPUe8!Qspc$?{LK!QoFHzT`47R=vGKpqjPSG^Id1-M2GWW3|qj;zylVXRkSMGIV=ryVXfYzB69 zUpe6b4C|`JO8`m#ZnR(Id9Y){1u-|uYsYN8Oy5O!vPO7LG8+C_J2qliqKVT8TNqNM z-OSSwios~Ww&6`|!Ki6=fyd{8eHFK>9Tob5B{lFf9HEg_70}Zvh0pKR+w)ogEeWQd z)z&MYfaJ}QzRv10?2JIC{wn}n__fDZ?AmauUagNvEMmeLF0HK_ehCkU5E?NoU~Mo4 z*CH$s9Pkd%gK{u{*DJIfb@CfHon^w;JDpYcBl#Tbt@*fm=4cY5ia}1A-?Sf&sM24z8MApIf|Y_y>j+T3_?zS zxfG{o*@Vwl+jFPQ>ecGV)xWF1)@}7715%jdLR{|UZ?SJawHDZebp)j@bCU~}ZZU}m z)*BxwnW{PyP&K}(!e0II7<|gyPr?2!iLnhek#RG7=>@*@zN$(d6gr(wGd;L{`0ei-2%=r8b5l+F7W0?>o<|SJoz0usjucFZ}Ypy%z zuQMvjorU@+LE`(sja7-vdK zkys1vN7?vx3(3`Zv(U29pC-+I$*VW%g!s0~te9{sAZt!0(T*RIavv0xWtCZk_~tkXup&W&@K_NQwPF|cPS zrAO}!-Bq9(w6Yo>x+aF(eHGR~(%89qp;xItBll}pc`oeN9|me8zS}~z)_pIk|4+cG zuE;&NV3CcV@cFaX3O@m0bg*-OSiJ3uCzr^=hscc+gr^uM1f|-L`1jn7h+9qtM9 z$yVJg_F6(>W?%8MYT?Uo#*)HVx8$TGq-aO`QhT4zI0Pq0_#X}2 z7f63s!5jb1P|c)#5fB+tj(`6P1novyaNUudDerA7T%KGVs38FcAW5tPN8)Uq-Q(K& z$g=4xWB|h#5+K`A*S9{;NM%=a!0hpmFjniW`?L4OZ9K2tTt;uhe#3v=5u^NdQ4L7( zF&}rKUkBoZRYC|8E)Cm!=w&xB_Xlg?V=9dv7j)ZLUV{0bWmIQl^qsakc$RHO+brL8 zoXA)gi%x`i)O_qz1}+qocy_sSXql$#kDmOqKE7r6*ZOGwNvRVLwfhLm2j@Sx+hK)& z>E!~CPXqBWLQ+B-l^%hZgzQ7}ACSp^KJw?npE~=$WlDuG{x16ek8&^Szv1csd4TNG zzq?!gKfn1at@M8)3;cfz|9@3W{@-;=SX8m19<16W|DVJ7-(8RS|M6a_J6wmhdO`lt z8L<%YdTw74l(mVg>z9G@>xJU~na0h`AihGCSMdV`?N?94*eglLff6PE=-C$Ze9u<1 z9A$@O3X*gHj_aZX5YbOS2}G=sng4wA1ZeGlY%>A^ zqVi9%dSs%>KeKy``jrB|VEft*$MJZ4@PYeYy-o)nM<;xL^a^}Lf7h+)24pfN-hlOG zdUu|-zU^?e;*Z&n?ppn*`KKJ!>)wL9dJUj`8fnEh`l|vW zSb)xdf0lpN|ENsi{?g(n>fJ z0RJmY(Yg6Q|5~gd^o{cBs~Do#|7`x|8PG3@=%GGEA+Fx_zz8{wmNQM!3Xt%DF#M+r zHcKRvx35vel(Fz+{|Jr$dC?mG)uI8dWX{nuHK2ndx?Ugzq_eI$B_4*cgNXrGjaIMo zj58n)p|Ag64{p`%ns)sI!6>3EQhya2w(L^&-#zg#UIa+>^Z-h}?I^9FCmCR4#*Qok zUpBEg*HJk}BcM=^n-#>%hPsZd-s6*izio^D8dt^M*MI&Mki-Tg>Wg>$2xhkp|GUX| zxqpVipITBRKYlz>+{xV`KLZ(r}lzMRfhhXwqSPKU;^PsLhoMoI&#c zN7Kaq)KA)!4M?Xrt~~hQb}m8)tdA3l&n<%Y&vh3lV8ehw+l`1Pqg$pCgT|5m9f886 zAmCfn2q6ci-(Xwqo&Uu7fL%tY(2#me!*i#*Q91sb^;VAp_EG+%Z+nn)-KKKbJ#U+G zp^q7iZYh7ywb#etpH~Se_U8fsC&JLy;4GN!9iCXlay{m`#*%kj0hD}TtcfyzFFiQ< zaz+Ti6vjhV{pzmLG6?|MThECo(;6Y`aJZSA+4C!_f?QqWd+6E<-%pvXw`WAYDtYXK^nxy>_nSdYe)gtMW$9F*3e2?#8IjXNW^ZQ9PI8mN1UROY;XWeRV-P%q_2FiSg z2&lADx@(6RD2ySs~+DT>oz%9k9G9BMjZx)d>RmaaU}eS*Kl9L)T<+v zojXGYDq8l17ItSc3ONF&UG00mzRK(L&ZEe{Wy?n2iq*dUca39#D5TKy)>aIhj$uKe z{#nE}QIrx@NHE};q$!%}=s@6nj6PAB;vIS-lEVI_-^9fa)l*N+pux#M9tr4)Th>>v zQ2kdImYh`wSVdJ14DEIzTqL+V$^6&Y( z=2nkBAXve!UWzIGXLfE*zG&M9s#D-$D5gx9=5g;3?!ahJC-hWhb&d!(E5q>1TjDSS zvD+5|#y5rL^+K^<*K9pVJ2%%X*$us6FNIofJLs4E=8~rTja9kq{j^QMP}JW7GP$a* zA@&s>>jdwAqrPs#x;LdDWQ{uY1S;iSeIm(lia^7(-_OtSXc@G1PfhrWoi5So_WSv~ z7U4XW>Pr2|i(uLK@_)tXQ~2LghXoXtIJ%PRr)b@{b&2wHv^T2)2;~3!DqU5f`9JQ& z3?E=IJp&@s$}d(0WLTr8wvO;AGWSno;nSUbZ*T89Qh0!L*SF2Ylz^R*t0 zo5#)#iAfH-V|h_o_*m_3jD6crUkV-8~8z3%&5 z*PGU!P|CJCOvU7#@PeS>^dIMFc_0vbJ4g;C=+el zaRd|Yoa1#yO#aq#4W0uaU$J<9TgeRSF~WkfY$CwAvGU_A2iZ51Is6?#fzB)@YedyD zWdUoWz4aFr*U+`Kh4yfVG9Ul?V$R7H0%?SEwco;_YyY%>xa-LD&u0IHO@Dgn}8;o?WZhRB@(mmX`H z%3&l%U4_cfz^|hO2dJF1zRF22ygDowp=;KN6b_(Vi7Oa6~!z$!BDu z2zv2VJz6i)68CiZR@7MKEJp~116E}8P??e!8m5K?DbQTzfP0!wF`)>j%S zbT1)^&sfpfCmSt9D>ZpsVnbuiV7ik4_)Ae)f(HTnJLQ3U<9fj2DM{qe?TfRh`j;0# zB4ru4LO~Qu<7e(j%>-}mTX$U&XU6J+0}XsRjx*FU$SA&wcnb$y-|nG>835cJApd+J z?)v+N5%#-~WwgYh$iUDu!B-6L0)5#}^?F%YQ0YJW-Me+bqVZjXB>}=_FR;JV=1k)k z4+5Dwvxq-yeoFH}DJL9PN`^c=`=QWMt?oIF)qXN!b#hed7-344cS0qVpju$Iv2Vt4 zT!l@4ZxjBfJ|-HWfRjyuIL3%libw|O#2r=)Ba!aX@(r4qj8%(C&y$ULRKY&1^A0QxO2K3stVXcmpod+BvECbHvS?^#GGL5eQ>tt=~Xmk?Nz_xj{_dI(M@}f) zvTlRvT089LHpGna0(Hw|kMX3rigr}_w%H@JGY=nOy+lhtTW8n$x>~$Cj^^3-KtCNO zKC*KZ|50jC2m{T`74v!T*_jjkZV}M_Zi(llEj4VS8xfun9J&Kyt%p0pO5)}G!urU` zPvYHO_N_uTF2Rs|#OBDzF)h{J+}d+IJvrO2YV#hyx-njN2m-Qb-N%W7-)A8OshW)f;yy)?%?Qh}r zQ(S6rsQ5nL53gk;kknD38$mg%lO82f&)M{NU^eex4%L_OcAxKO>75FOLFtZlCS*kx z6X?ixM^4CYyU1lr{^{=fGeA{-$`D{?$pqS@x!6smJi~DF~?U7Fp1BJlq>1qCxc&sqJ_y}m=eWGcpA9Pp!z zVE6s`O#o#>#Tz30AheNjPYS-;n4eo8aR|=GvpptSv%w4&&b3oU)O~$$bv*$t z0xY{Uw!!sGX0D>7TQ|p#i8~6h-O5N1LeK zD4be8^MShH^`*1k#}|kCf(Ly$3jx~r2$l{)a&6UV$&_O3Ntqv3wiwsI2n40`a;b1K z^G}$bOHIMI(;_yxc5L$fGcOL12YkGx^(Y;thPi9W{G!sq&k4$;J-2QryIxzHx~A2~ z=%VY%^rm|`d0GkDi!uVm&2TE^=~rUcy69{PyiD&;o#fAKx%|)4yx(;s^4G!!?XO_3 z3`92^nd`){dg;A5VC|Zm!|=F|6xZs)s+AP0X2t}0-|27iNhPzD-u zSim*6;LL3Q4y;PI@r6s<&^QPN6XxUt1y+GyUWd}m?=AI`Dh@>di5{yXuE z{Qbx)sir#x>vHZTXHJzFxZsx4LHu!+G3~)^i!~0r!Axc{HGlqIO6;{*qEV7j9b1RO zBokZy;Ym2j<85ts5`}Aj=UuaeSOF+Q)lgFnpW&YJIycHeYRh#n{)#%yb7r&$h)&%j zt7o+6(}PWzMs3C-&SVtAfUYH)7}c5+F1E}B5+ilsW2-m5nE1r)quin~@ijB-CA#>_ z4qC%{{)`ITl^QiH8BokQ^E9D2gGscYjB>1_EY?YI%gB(T$ZQA3hXIa9j`lm9KIhRg~0-8VH?U@*#VbwPnMEz7mQ=kyqbv9v#}m4Zm@sA zDgT~>x7E6WUokd2U)_$rp=z@b1oAhx;3t zQn{pxx5h}%MwMSsI}?_OIIy(lGbqTf&h$D+SFrXQ?mVEo&MDnHyo$$PUb3|n5YM$| zDLg$}7#r(e7e{EnLdf0hNqCU4RNlLIZj~9c1t;UD{5H@TjM`O=s3Dpvs zA6>@@MgY%q1`Ox-r_egGg?v(5>BV zs3S55_$1HpD*Q6eIq+*6iNy=uRJ&#IIK(6Vexv<0Ufww04$BRid(=;u08~`q&ijSn zlCxsUUfoewCGsZ9fk#E$Y2K1@l#etsAV1}0hVv2-XdhYRb-uyd%!yK6UV?s5B%N$i z5`wAS5;=XRXW0EH^7S&t`QN!nHntP!pFLezo-XHIU4oG^F}|}nVnAU&$ny%_NgAOE zAMGZke*#ay^HvBWMIf#9Ks!))ZNhDMSRr#T0vcRK4X7!*%q$WHE%{BP4 zJ1xilwpTebH`dB>$VouHuBf*)^jv4*S)lpgteyiv#-wxOR%~gBIvN&g@Q)fZ*L5^Q z`{R3;zk7q@!`Ymaj4PaXPmfv2&bzT{i%&qmqGQG!%^SO9UZ6{ z>vb99=!VF~Pv+e!>zAc@_xdZ5O?6|ulLYr46btS3dS2ic8?f8W+UsM*PG6cmT$)>V zN7}i5WE~$uD=EHRK3PoQQ-leQ9OR-4uU;p{wp=5nrzj%ZXJjD?YL2}C#mHEwOs~;0 zwuJtG?b~yCM2M`!J&UmlJ3t03RdPf_IG}8sj2&ta+ zJV+ojN_|u3287h%ICa_L^U$f(JGpdTlC?9NA%^qg;Qz-{RTwX9Er;r6tvCDm+ zr`#gci@*HSIX#oDJaLb)zPQJ(HoxkNKU zcY0*bRR)F6>#ERp{9ZBVLj);uX=Nx*$XydLEK3F7Jl35%@A?KZ*u{6BCyKAbGBsvY z*G?p76iF7@okj=`c_^yxy^Jk{IWxxWkrxGA~cmmtbD z`I3)%76SJTeI9ZKmN*M9gKhTNPa0WTWML$>S)z$avX9>@KcPl(`H;%H{+hil|7hNuXPjcXEJ`orq2w5$ z(RYuvjsT&jAKLFte~{l6pCNz5JNq`?GR2ppR-fBV+VjyNo9Nu3;JY6iXL2IS%#Ls- zYHf0lHC2`Wg;uhYWzW!xoKhj2xGcAa%YFTeud z{KF+Sv`AAf076|`KnknqG;q4v6~ESZ zE}uQU*(YeEOP}5Dg&{cbz6f5qKo1*gIXm~+&N-}FD_@WqYh{wzaotCZ3iqG}bu3if zhuFt8_=w!poB6(4tl~Tf6P7AbY%&dHyWupob_wHnT&VDS?layHQdvy7X{GVCv5c_Y z)EGdpHx-P&hwQzbo5#;}n8z=8(Yfd1)4m%!pL+bl+X_9w2Y~6enItSk$COADLd%u+ zKKtwxPD?dlv|G0gTc}jbCy)b#Pz^5e+O#>v75Cb~tT{9%Px)90?nrH+(5jT`BAIb_ zJgeavGUjtJFfNsV^DhSbOn0=~r@LY+Ohm+mS%6yf7Pw1Y%ym}06atMjtxd!KusnzGXc=U z>U2USU{Pw@=(jxmG3YG_-;piPmy;k8=^KJ$i@0s+dvR&#&IA69YGpo|VPn;s0^^pn z^OdtR8+()nZBhyacb)dY)=M&Crsy;=)f$F{S=}q}WID=V!e_X>fu++p9b?WA{CF;E)BDQyJnfY=kDnDWp zQ@O60BVZHq&@%L5K;8VSEMLH64WG%Qo{31yf>nUcW0o7%&cU6*PNB&5+~L4k=mgKQ zv{8b+Sq4o`INkD4%`FVljX(aVZYZx&D3sL-4MmNjy75CBE~s?Ttf2JZ4q~wEu02n5 z4ln*Kl+#lZB4Sk=`k?MhhUQ=Ojz5j^3hw{hYL2%#Ka`k=T$#Il?c=MMj{M zm-?Qms(8C{h%(DLTLoA&bSdr(Lcw0q8NHX$+>f#J1)G8mgCt6isqniL2_J;&DZT8QOvmfrf@paD7Ta@hB4|`-L z_degGI@PV%Z7UUEKj^<>&i~FH{!KAFKjSteEV`xB-+JM5oesK?>&@3tuFtJ7*Du7T z-;;cOf862TbaB^Lm;C#Tvk6G<_7jdWfxK`y3rbNEaYae{<}?ZfvDqI7 zLxx%)oF>ivl{Z?7WZmT0w%UQGEX7etGycv>-y|5JTdRQT>!&` zy}65E(w-L)r0~g5S`ykkgn{&^@(lPi$?Bblru(O;K3fx1Bj31;U*g%fjI1=t-LjH% z*gbNYv)iR-PqHmyH7q1#EfoY?t{6gAs#)GU{za=5VY}R zlbr2wb7Gho9R#uf=4{d-B}1Cs5)4p!xkuT?`it|7LFJqEuFXkSqE^T27f?iAn>l|( zEKe*9V$KNFJg?NtL(#q&FoF{XmRKH6`zWNjWK}1L&Oo5i`5=V^GrB|7eDK&38)2qw z#i}gy54DT@V{jDdx(&#L15Q2$dF`xHW_0SP-S#wq!7asgp9Lhw4v5CTQvUuDigGwt zm$^P=w-_OE^^!^W3vI&4SH()M$3DSxIBNLZkG%}W_1e%MZbwdXnQgR+^Y>rymuS6O zX0>^!(^4Bb?>O)(|pc)Y`KtFOaqx?#h7WHY&PHexI4+qTd7^ne8u}SmyEk z7_8W6-DC=L_>44mUNYT;PVRrKTf&RBNz#OYm2#Z98xBmz z*M+>`nZ-?#I&Ez`z)&e8VCyJ1t{Z7g$c$~CY_n7Fa9i4J80v%Xm*D>ONYFzh%BU(; z6*uBtx#$V_&hp`bds;oDTVirr-T|jYvw|Yhuxiws3+%J9jhb&$9our_7AYYqj;F9_ zuH1!z_|{ouOZaazk!rCw0C)L=hCuW0$O9uJN4=rD%TlwqxSY-}Qh!9^_F6*_c!3Bz zVuc3B>&``WXSh##rG~2F#;2^LAd1OGZ+0gmsO^>Cp{CIjge^Hbo-?ZwCYln2+Fz6JTn> zOIlnp$DVmaTN(y;mbn%>;367Khh zf*7;TVXY-PnyBgsJsGNMIgQ#d!buJjh&5ZdTF3%xMh1Z^w9Ro)ZgL*$#Y@nkNXeoRWF?NH; z=7RjA$=cke;*Ab`^=Ia+wo2XjHCYyuO{u4ssg~R9KGyT7m!F(IS8baCu6TBJn6z&} zVBhhX+hJ{fW3TlrBJ-=J?!~&^5i zFlcZO_}m205Kegj4NZ}&K1IuQ-4n?!Dt!t8=Op0Z<}Zwa?Y=R~G2(Z42Z%l$QH4~! zQvcS`u>wd1z^?k${wUEuEC5JlxnuzhC|EpBGG&uVOGW+M!0R?0of|;m$~`UzGc!Swp8C~YTPiwr2|egMR!X?mx(AG zC-|O)Aa46=(E?us#*-3_1fW{YmYjK_HLnCXPD0_~@gV&} zn#QlWqcgaVyz>#%;VWOOv?HAhY%^~)2HpSZJpb>y?vj{Y=uFhZMb|Z#mKJtk;_P0rp*DKIS7-R=!z(E zuy#{UbofhPaY2;P6q(Zc_8sNDg~aj7p0})xd*;{D(M6ez5~!eE;Wr;l`MHRi+ir04 zc5EcK&zkOhyyDivgmdxe(_TN|L$ z|2*p&COnmWKc&v`x|PqYWTKTEBSUZ`@N{Rbau}9i?@bt}m1qg71g;tpY97pOL0e>T zUPQw0a;~#5w}K~}yl8)frwt4;!FGM;f)Wn(m=r zpQoyB8?2kV&1s(f2w7N4X$oEjg#LQET-@Pb3Kdxjt#u7~#*UMiLZ#rh8wG3ESnntkB z^v~LYhZMYrxi%-P5Fw5ed+L=Mrt_OLDT~rM-@uzt}RO3vll{ zqn-nA;Q2S_#MC9akTk!jobQcPY|TBND=Vgh*gcZ32PoU`o)Ma-&Tp6MFcfV06bSPo zK*n`EgI4i5`m2QMmpUyB>*S#VnF4-mP6P=?wU14YA`=a-lssx^D7anbBVr=o|02Q} z;abxyxt+8#IB4gr?3&4kTPHcTQeri?7ozt%5NYPplZw9IC*;`2BzwKz{n{lUb9<)6 z@>?@7Ra9sn@)FK!r&Xvp_PB> zP_rAIdg*}M$%a4Dd19+jrE)$4JDF6i+p1 zt^9DRyft=vs`9$4q@&)5Wg9xWGss3cDWoVWb^h+i&9yh226hwlyjb%#^xSrU^u|1$ z%}N13`2)sy9xVy%+q{pJVa5Spr-fQT6b@{Y{Kv*?&kxmlGFS!VaU3U9IP;Huics74 zn;9HuCO5@M1tYP<{;9#!>sY21@wr6+-mh(68pxB)Z)&;Aw;*XIkthdVHCg%@dh6p7b zSvBlmQ&*_Ql$&=KJb(9@lEhy~vsJoEwfvB9K2@UHbdc&MT7aeqZZYGRED@`9#KeQJ zfHe|lGjh<9As}ByQ+RR-dsI7;9~cU08LPQka-_PUPQMSv=SU?_TfZKo)n#PMc?k}2qcX485gkF5H@ z79S&-D}ngZb7{fr8a;BJH*tC(VGT(&L9ty>a0b_75N0E>l4v=8AOaoR7|f|F_;7~N z-szeWlo!q8gF?(?3(CmSZ!;o3+Jc!hp8QBDh}Nmzyd&RDjgcNLtUb@9slVzqZ>8$v zKKM0p{*XlI?{~9-!U3;e)|U991*)Q|&oX8i7HJA2zbk6(H^W+5ZPvHAn;>$Xx(ig3 zP=;_|8j12Ew5^=wQfRVTfJO|H8G}|Skf(UFpQQ-+HGWrpmwOb0MU1MEAC0jSo&FxR zGKg~fmpl3Hn6i?plHga?AWHbbx5M~u@ZIDWuZav|e>_E~_s#A`1WdiuN1~yS{7`!( zH(TG*mzTD#Q2P6bjK|{Vd~S3xk=I_PHyaYSO!r#0E(C9Exsn8I$pRU-Nq`%(5N(uBk>3L4*KgSJnZYTZo_a)9`JHi>7y0MMDq812J!hQLppLl%^& z{-#JNLIMv21t(#RvZkQZFd4sP#f1pcSE`oaVWUc>3Gjv`&GNi^1JCo8z3xpQzE?W> zk$B(mXechpxeV{&A!X1%e8~T#1?KchCv3I1b&X2YpOl(%#>&DnSai>kg(s}rxdaC|R%uBYSEe^E=s9{sI_M|Z;Y&O|~P zLvI;rI5=Q`kjFea4JY?3cXN?q!GZU=r=Gxy7P68d*h~{cjNEb-yl+i>|GC&~eS}%Z zq!|8Kx0M@^iPeW((najAL$4$&a!^R@f0r^?)!)w~$EE0zReVcfP@DXql%=s(7skkt z`E+@W<>uk9$@|0{?O_2Y$oT6k9bj-L8`m-Fc1Ew8V#Kh}yN`WB3x+uqM*_oiDM697 zb886afMoLYhaSV^-PFcGS$WisvF|=-bLmsa%-vy0;tY=(G^W$u4o}JFhrD3eX^(ogwnHSTgRahy!(no{rqEnY@`B4}< zQDxC;rLv_(M>%Z8Dg%41*xh=8bCQB$h1SbA4(20=#EX4*MFO$`+mR;=ExJQB1J0|6 z4xgueA9!oRy3x*lTQ#HR_hi;MNcop>tilQG!u#k)f z>Fy>tJ)nvyk$Bi9TvvLg8SYMWoZnNyuqYkbbr6Yz#D{SuGH-j!hj~UUlZwy=bUHh` zlji7NdF|8|``-Kb`wB$l?dG*LMHt5rFqZc|Cil_`YEB-heM1!xjaNutG>kpR+MIVL zt7KA-T>)3a;|-CQIhRZoMY);^Kl9q$PLvu@#IS5fEC!f9Fbk)WF3GyV`juX#Y0P);mXOqc zxchMAnch;=Y_b}M1d zdc%g~Rd!!v()`7^3I$Z2PPj28dzI(@mTa(4Py_W3K-Qs%zJ)&^r z`(THRJaRqXd{D1xJB^#<2Q%tZ<&`hcS+*k7^QzZ-LECK8(^b^XQf~1{u_4WePs9B4 ztTJJeC$X_<3*mw^mbjI_f-1VeGZPCu?U-iLs9CA&Z^XMZ%MrL&R?UAF_0|daRc}9^rxI032H(V;&uIZ~muV=Q- zX;aqrYW>X5w`!rCJiSU6jm68a6z9zV6%oxRW0cBL&ywaUTXN`p-YO1cXdE24&VTY; zJQiT$=ASus&)|a2!r5VY9aw1uIj#~tS>^Yb6bs38v`;auurQwPU-%QJgRT6vK8^O81t^vwY5Gy-?3Hrn)zn3 zb%a4j-t69Vk-?~Dd9nfKEqB3U>aNQ&Ygp5i&T6D2abS-mYuJG9s?Cam=8T)=!|{F0 zXzCbEh+JiM(my0V8E0Zu??Gf!pS+a6Lo+2kCzpCYN5{-0k3S)nB-Tr+&!`tO+!3Q= z#4V@h7oMH$??gXh@6x~l6HC993omnzztMXiYsUQpYSR;abIN7M4<_9X!lKIdoU~DY z*JE}BG|N$uQI0A5{Ym>MdKLZExqYyN$^4?)iNMdUUjfbdq7tSsa@MfkL}oe?sjH(j zB{m!E{obW_}yt`|J9B}r) zSg|B!s6Da9M{hVQ^o3X<9XEX0Y9q^v?!QZ+n$$!yZsfS%5^sN3EG04y9LXN5p!3l} z@;sKVZR?~UsKX&CrdS+{(+MWoHHJzO`%;v#ILcAGkewAeLqtp0a8wlSs%Y%DWp0|< z8go5*v$9OlnUb8z9{1Yk3)sc53~jmATSMAV}uT<6+G3f?8w2Y5TO#0Kdxe=bHH2!il9Q zKg!V%$X_}WP5?PDG?OLFMdEAVcm+bB;b<957==jmnTAH7mJjGpOSZ|6pPusAk!2`C zGUNpKj%yp@m-;r)&Lg#X&3B*r*ux2%u)o~QQIz5e+~cm5?kE0E5N}~kq_b=*#Qg^Y zOKkw7FNf$%>*fTdk;)dfTBD1Z&?CIVx_0|FE}f6DD#zuye~zS(Mhr6dESFImBg#{> zKeKD>bP&Qgg~*H+o9JciEtg4N!iYOsTQkUuF~H9Tha><)Co^Rmy(wgApJ`7)im-AS z_6*-UQ1=Nh^@9+QQd;n{4xfMO4F|-0la7y}HU6YB)sQ29suD$yc7|!DR(jsmIpayuxWN|U+dH2K-W6<&C^0X#4}txo@;S9Yr5F!*N5dQn=W$PRr&DY zxRM<5!jJ(4Z3wQP z9keZMIws5ewNxvhzOy4yR4vanOa=jklG;GdJv?hL``cIZV#IrjUK4nAEPM|M^%W?i zN*t$6Wq?_IfO3gDZRv}ixHo2J$@5oexPWGQ?Nnk-$c#e|-`9EBj?l1(xh1g)4VOdC zUn+NnM{m7Vl_Hkyb;eVz<(pVRV$Om#6?}SL)DEHbORHxR#k8Z^v`mWtKO(Pm z0jtvR$>k&k3oXA2`*7d0|M9VTX@VHAX?P291zia4DZ|7k)tWMr$zvhKh?LF&3ohvR zKk`{t@O-WNt;MiM2hyu5eWXG;CATok*E}4q z$kzN;7n7dgxsVi;*Khs^0=7+Z-t>0Pwj7WM+HOXD4Kv8VIx>j_lPZiprnH#0r{BN#yuYUDw90M9Y(?IcM&W3V!tT-5eg|tHiBUIt#9g1ytin}x zy}=_|3Z|%JFV+894%VVgD*P;p`$ut89Xo0V2}Ti3eXQ&{q3M?N#SUQ%gA3r;>E6|l zpd;kXdB-a|#)uR+>YZSx8@anoZPau3pE-_-!G;q)gk?LzGQ9cMaUo4XMrLt9#R7uH)Qm!PvDT7gpEMxGIc`Ev$psKWe5H|U$9hnupGrfeiYWak+jf6EZ zsXk7c*s&}H)ahVy^K7#BK;iD5^Y9MHsc-uQhIC)lEU_~FSeQjcFL_n4ctYDsk*9uU zqi}~(1Kg5Xla1>PL>TLS?h9;u)5hOvnEla2=S9BL!pU|X6`oSneG|0-INje*0ha;m zqmG4{b*dG?s9(bJ#)`|6vBIc_{49idsFjm#nZxGU6^}r{wqy7*j|16PO&jaVpEg4; z;wj6M<}1q5$P-)HMg$semI-Tz1lw_?$aIbt9{pMoQU@qX8u{eXmrvOR_%gts+mvws z;ICssrSxohTcMLvN?ne>Y`s2SmZ79TOCQ%JSS zSmNPOT6i&eV6o_U8x-dLG=_lD?DIfwLEA<5dF8BCRPP& zh+Ac_U#BJjCo=;qzj)$8eRxplE_nF(Fi7O2NnO3G4a!B=0tN@8~t9Kh^@ zcz5q{ye3wAbKJuei8vbek@Pz9UHf(QPn}>Vy{r>ZF3z2A1^={pj?olqoA^V1P#3?S z3s(qGOFij103=beqZ;1K^EY8YrezYBDXf4yGe_TAw4jbRrrgA+#JS8vn zF+}U)7gqvJdl~wJpZK$4LBgLPFB&*?OaC5Nu5@@2=vq~A)Q&5{MCru6&=U!Du#eGU zkFE7>JX@9b%9!vh{G8kQE>;g-W`%^t{u86E-c=0!&>v5WJfGO^WN{s)Mo9@E!%J@z zTuazj!I$DnadQXA5m&q|&P5Ki89E!qYXmH%pT>K>aE>dSI1&wdrYeq8IbiaI?bR2z zn)7ImgI$`b%t3B!SVKEBg-Rp={*^Vax0cSlwlDLRX}wib7T{x*J|COAG4R%-(`hw4a|rJ`SO zP8v}&ty?vkae^QRt5WS_e7-?P@NGGvaZY5$Oc^Ig(}?;vaJ&DFM%(TakIG=ywRZsP z?f@J67=SXk4@$}!7M=9`8ft^tflSUV*d@&=xPRfR`je0|kJgg+v@pV~u&tyRd^u5! z*ivS~+QbSzg!WpNYF#Z)pw3!f-t$^F|0VyF_F$&7QPE?jE>6li3%n#Ff%?4XqDTPw zn=hDDLm04hSFJ^zYY1KuAWBTzSW8MXP^H_{a4-+Q7kCi!YLmww=-e`&B>`ybiS&00 zk(dkAQZ)dyO+4}YQqFGRK$orlNTwv>C!;gdH6$YBq(n|s_5su;E2#G!31dxx0c{53 z^l*>8W7=ohwYUWF^8>Om^q^PU&fDUd`4!n)Tdt5NjH$0M)gN)aJOEe4(k|)5rsGkX zUFuIF&R${_GvL98Uc~ryOjp(2#jqt~t8uh@nt*_`v7B%gHa@mAN$D91&;dkm5;+u>{k{8kQ#{tS?fEJWU2o| zyUr9#kLdBZku@B=NoO3_quxB$nv6K0xYW!;@F7T-DNM6yLBUA1zV3$ST{O4b!=t^5 zyAFd-EK=`AwV)&hg!7L^m^>%;zwdQm4Z}_m9!%q9YI^>oeil}e{cfjaoXTOL5#?Qr z6i{@`!x-vX@9m?Z5Ej4nVs0jF>O_;P`dDS8_u}`X?4grxUrfe{)>Nfevk3;2O>pe$ zhCT=6I;V;`0n>Bh{A06_ata)VgeAviz(;kZs@o>&ggusMKP50ppDp>Ql}FKj3a@Vs z1WeJ`lktkt&Kp$zeXn#a6Wu&Z2#T9&AdhHQHFIl#TBd(-u#hz=$BZ9(fO%^Aqi&Q~ z131~^YOqs)c}gy1nLE9%Q8~tc@uSUI4}17nY5~M7&Q))$na)4k_dg-44q&S=9z{2-34HFj>bzgX*WuC)`c3aD zp*%}=T*$1_A6veYzx&B~Wla0n=P_q%8YW8eAAy=?kI?O-k2dELY@OAv|8Sd%K?xHE zoVn!FMF`hZRIW3UaWD^u@12IcW8kn*Yj&jkjBFgC$#E+(zfxcxM_((ZA89GM)cpq8 z`q+kyH>(bi3<1blF@@DwGKHZg>U26mCSBM?+;H^_3#MU&^5ghfC~bQ#>GfK4#gV0~ zdE9^4KB>`Rqg1|ZjA6WlftBe`5ZOw?pp%cQ^8}ffxvt7yt})$+#}0sPVCmtrh&Mxi zw5JL2v*&TNS=-vo-d`cMKRrTNq zP9M$udg>r<8ZA}NXVJ11a8qz|a&z1B-H6G~NtU?|SH5SG}egHCoKPN96; zM8Zb&8?_zfx4x$U4VvJMXB;e?8|4({y)#x|GJ1ll0)S)*`)6^QMGsOk98QdBkxZ%ptZWz?5 z>}LLkyGU2V?o~$??nYey8$ftZV*rvG5RQf~mazp-ThvwTS-IDL$5h%%pK;9{R*2>= zN0yn`W#Mmd!XkS59EZT2ph>D0&`AJzfTRp&qGu7}1GH$K3(z__eNQ$n&1=wnS!S7Q zS5!nINmTT&v#QUD+(}*X)%H=yM%$7xP>T>}ulU>+OOmj{-r`87V5Lg*->%_gq3R0n zv6Qui=l?Gb0+_-?h%YOV(l!wx#V5niS>Nemau>?i*I7x<@6F>s{ZJy$KFadkBhkD` z{peacNP06UhGUlu^nR?gvu%|*&Nch?omFUN7Ot>>`*3!(+KGZ)6vphrMr>fu6)jGk z1%!0VMRpZ>XCI9Oe0d#H(gVxy>0{&#XwJf^-f$Og2$h@-(dKBQu)YT?9-qfZn(D&k zU_w?s4e|-9+qM_R7CpXC3u11yaBH6PWXP}qqU()<4$lTpSYq$d1)po=|G_qtZMPXH zm9;`zg~qocCBx_cH_tX?3D@LcCV>O^h4>T*vXoq5$N1LNqpmxxTr(l=?JT4WlGnal zOhw#5-sZ^fkHJ(rO)%TCFbZAh<-gf={1})dAI8{Xq-av!(dSx`2DK%FUf&nb(8>+p zr0y%9D-io~0P&H#)YF~IZkQfLGXh)_`~uWI3NzU_zc)SkMT$r67o7Q?OFOYGBJV-m zSJ#(B`fTl>X6jZGwzWP= zJ5-(fDWVa-RsNKi;2XE@S*%ztIq9lC_u7RK1?zmueAU4x=Qv<4(7) zew6d1Vb5ec>aBqBGg`|McQYI}XI^{9DK*3VzCB4z+bBI(GYg!|O(w*~lE}#U#BpwL zbE=@qbQeECn~vQX@@(&WMJTh@Do)Quh4~(*^YxI)>`3tjYq_nYII<~=wJvlC6ZQW5kUV~ z~Cx=g%Q{s1! zcSfIXgFgoAjKubbYf;Y|6NhgPH}4DuUNWWh^e;kS&SIHbO6#q&9|OWN8udr{&Rr-= z!l!2k%bTUH!vIUp+!_DiUZBOdCGY>I6H~cu+Y+nI&7+>zmoD&w47FVUwnG zCkL87_0N>?KkaAFMLckPaf80={06BT>`sRL97iP;FlOvJ_Qz4>gRRQEEvM5&_@@QI z`%8{KMrJ<=tIREA4o}-Ag1;^5P)#(wm9FTeSxVFNncJ$fKxNr&YMFr6LRlaKQz5^A z%l2G?qLBc(j%rR;-QQx-^=?)yyDR?7BuOu1x=pE!={*YRR{W&m?QLRE3kn~uh8g9a zFCWsJHuSvOP5t<&#r9kAoA#xx=&K#fL4QQ*p+1{q4cl+FLr>#xw`hxfK0Sfy@gpFR z6Rsyt2Q;I}J5;3#O)+2Zbe%VC0X5zd0)8Gdvwt$)c^$*f=r_ZzY=K(%_ACB6rE=#R zINB>PD*U%yY(6H`i_1diAtAIf#-6NU#}yvak3V|f3*Tp-(Bk(m%l>Q)(bRctHhc>t znT0o#=RRo;1jMCquv{bh1wzqLH$9Eo2(5%RS#N~mM7i&Q1P+2#b3gGOR2T4!xZx?A zQ^%4MO3yO-psUSf~WSUkRDb|O7FeALo zXL8*j(EjIc#3OzkOu8w*Ko9ES47;@+X{@6`|75(~weK_nfL(F9dxFdl_s=Zv0;{(# zb0yN3AfTTwZ(YbzW;PzkSfWF-D`wf!yYK5v%F6nzsY*FGV#!aYzHdv#$ga!{U%3NW zSilSqt)X{X<{;TSs_)a&Z(YbdIob7^j!$ne+1qh}=tgi+#i`xwlMWH09&wSu-x(d= zy?P;^83FVAFfyLl7VrZs<%kxk&$a}5Z5g*W%OQY`2 zVGI|r>Wwt9U?iZ`Cd6%mh3%O;<*6uB6u-Ua^9OTe}nK(CcOxJ6{ z4IuGtt@BG0BqVc_;i9@j-f%k&ah}-P;Y7Y*tZe^H=OgQ#aPEMEoyUX%)mwqKzfl7b z(KkKP3$hQ=#KL=UE5xg#G*Kd=JF8oXFGEY!Gu426zp!r-XOKH=x~-3^6n}F|M3hVY zyW1jCTE3z}@Jd`B-4quxfg+o`t$y~lAAZ*4LX@p zm}v$pFcvw5ahpa5`K_5(y{@H%-Pd3gve**7K*CCMk;eje$PHfnA$({|@aWjC-QT^s zzHL#EbscIWy3&_y>7@8VgmG1XzMbi1lbyaXK;6RV4+Y_x2Dxc5==svixhGj*MW&wb zMJ_yp;)z5Xjez>zluYd7@i2iCr#l0>0f-Le2zBQ)wPRF9Nz=qHW?j&muD`PT-!vVVJQ87`38lcUsoBsKrT#)lwzQnVACpq=Nj8#IUeQwjgj$`)+`HG>#tj_CB zC;O8=J8p`LKe=!HE8~kxdp2}-E)4zgOAz-c)7~}TUoz3L2;r;xbt%kIf|03!ZG=N<?OhlLL@eLku-;`)?rv^1225S3j2Iqzu@ELWL^5S9MV z>7dk69n1vf`t$$t!KYhoG_U`QEG*ND@L}M;X)b;k#S*K~{CCn}-S2J49{l$^`m(hD u&+Hc;iV$=Ehrckc0=Db_znGE!`}n-8i7HP*%~$`T>@?MMRm&dOg#8bu#L|BN literal 24457 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. -Adding a deprecation +Adding a Deprecation ==================== .. _removing-a-public-api: -Removing a public API +Removing a Public API --------------------- The simplest form of deprecation occurs when you need to remove a public @@ -49,7 +49,7 @@ Under these circumstances the following points apply: - You should check the documentation for references to the deprecated API and update them as appropriate. -Changing a default +Changing a Default ------------------ When you need to change the default behaviour of a public API the @@ -74,7 +74,7 @@ API: deprecation warning and corresponding Sphinx deprecation directive. -Removing a deprecation +Removing a Deprecation ====================== When the time comes to make a new major release you should locate any @@ -83,7 +83,7 @@ minimum period described previously. Locating deprecated APIs can easily be done by searching for the Sphinx deprecation directives and/or deprecation warnings. -Removing a public API +Removing a Public API --------------------- The deprecated API should be removed and any corresponding documentation @@ -91,7 +91,7 @@ and/or example code should be removed/updated as appropriate. .. _iris_developer_future: -Changing a default +Changing a Default ------------------ - You should update the initial state of the relevant boolean attribute diff --git a/docs/iris/src/developers_guide/contributing_documentation.rst b/docs/iris/src/developers_guide/contributing_documentation.rst index 96742895689..56d2257a55f 100644 --- a/docs/iris/src/developers_guide/contributing_documentation.rst +++ b/docs/iris/src/developers_guide/contributing_documentation.rst @@ -1,7 +1,7 @@ .. _contributing.documentation: -Contributing to the documentation +Contributing to the Documentation --------------------------------- Documentation is important and we encourage any improvements that can be made. @@ -28,7 +28,7 @@ The build can be run from the documentation directory ``iris/docs/iris/src``. The build output for the html is found in the ``_build/html`` sub directory. When updating the documentation ensure the html build has *no errors* or -*warnings* otherwise it may fail the automated `travis-ci`_ build. +*warnings* otherwise it may fail the automated `cirrus-ci`_ build. Once the build is complete, if it is rerun it will only rebuild the impacted build artefacts so should take less time. @@ -50,7 +50,7 @@ This is useful for a final test before committing your changes. have been promoted to be **errors** to ensure they are addressed. This **only** applies when ``make html`` is run. -.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris .. _contributing.documentation.testing: @@ -99,7 +99,7 @@ or ignore the url. ``spelling_word_list_filename``. -.. note:: In addition to the automated `travis-ci`_ build of all the +.. note:: In addition to the automated `cirrus-ci`_ build of all the documentation build options above, the https://readthedocs.org/ service is also used. The configuration of this held in a file in the root of the @@ -112,7 +112,7 @@ or ignore the url. .. _contributing.documentation.api: -Generating API documentation +Generating API Documentation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to auto generate the API documentation based upon the docstrings a diff --git a/docs/iris/src/developers_guide/contributing_getting_involved.rst b/docs/iris/src/developers_guide/contributing_getting_involved.rst index edcbbaf7260..0fd873517fe 100644 --- a/docs/iris/src/developers_guide/contributing_getting_involved.rst +++ b/docs/iris/src/developers_guide/contributing_getting_involved.rst @@ -2,7 +2,7 @@ .. _development_where_to_start: -Getting involved +Getting Involved ---------------- Iris_ is an Open Source project hosted on Github and as such anyone with a diff --git a/docs/iris/src/developers_guide/contributing_graphics_tests.rst b/docs/iris/src/developers_guide/contributing_graphics_tests.rst index a276f520d69..8d8189c69b1 100644 --- a/docs/iris/src/developers_guide/contributing_graphics_tests.rst +++ b/docs/iris/src/developers_guide/contributing_graphics_tests.rst @@ -2,7 +2,7 @@ .. _testing.graphics: -Graphics tests +Graphics Tests ************** Iris may be used to create various forms of graphical output; to ensure @@ -31,10 +31,10 @@ known acceptable output may fail. The failure may also not be visually perceived as it may be a simple pixel shift. -Testing strategy +Testing Strategy ================ -The `Iris Travis matrix`_ defines multiple test runs that use +The `Iris Cirrus-CI matrix`_ defines multiple test runs that use different versions of Python to ensure Iris is working as expected. To make this manageable, the ``iris.tests.IrisTest_nometa.check_graphic`` test @@ -64,7 +64,7 @@ This consists of: against the existing accepted reference images, for each failing test. -Reviewing failing tests +Reviewing Failing Tests ======================= When you find that a graphics test in the Iris testing suite has failed, @@ -122,7 +122,7 @@ you should follow: happens, simply repeat the check-and-accept process until all tests pass. -Add your changes to Iris +Add Your Changes to Iris ======================== To add your changes to Iris, you need to make two pull requests (PR). @@ -155,7 +155,7 @@ To add your changes to Iris, you need to make two pull requests (PR). .. important:: - The Iris pull-request will not test successfully in Travis until the + The Iris pull-request will not test successfully in Cirrus-CI until the ``test-iris-imagehash`` pull request has been merged. This is because there is an Iris_ test which ensures the existence of the reference images (uris) for all the targets in the image results database. It will also fail @@ -163,4 +163,4 @@ To add your changes to Iris, you need to make two pull requests (PR). image-listing file in ``test-iris-imagehash``. -.. _Iris travis matrix: https://github.com/scitools/iris/blob/master/.travis.yml#L15 +.. _Iris Cirrus-CI matrix: https://github.com/scitools/iris/blob/master/.cirrus.yml diff --git a/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst b/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst index b01f370ea2c..3e7a9f1ae38 100644 --- a/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst +++ b/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst @@ -2,8 +2,8 @@ .. _pr_check: -Pull request check list -======================= +Pull Request Checklist +====================== All pull request will be reviewed by a core developer who will manage the process of merging. It is the responsibility of a developer submitting a @@ -38,7 +38,7 @@ is merged. Before submitting a pull request please consider this list. #. **Check the documentation builds without warnings or errors**. See :ref:`contributing.documentation.building` -#. **Check for any new dependencies in the** `.travis.yml`_ **config file.** +#. **Check for any new dependencies in the** `.cirrus.yml`_ **config file.** #. **Check for any new dependencies in the** `readthedocs.yml`_ **file**. This file is used to build the documentation that is served from diff --git a/docs/iris/src/developers_guide/contributing_running_tests.rst b/docs/iris/src/developers_guide/contributing_running_tests.rst index cadf3710db3..99ea4e831cd 100644 --- a/docs/iris/src/developers_guide/contributing_running_tests.rst +++ b/docs/iris/src/developers_guide/contributing_running_tests.rst @@ -2,9 +2,14 @@ .. _developer_running_tests: -Running the tests +Running the Tests ***************** +Using setuptools for Testing Iris +================================= + +.. warning:: The `setuptools`_ ``test`` command was deprecated in `v41.5.0`_. See :ref:`using nox`. + A prerequisite of running the tests is to have the Python environment setup. For more information on this see :ref:`installing_from_source`. @@ -90,4 +95,93 @@ due to an experimental dependency not being present. All Python decorators that skip tests will be defined in ``lib/iris/tests/__init__.py`` with a function name with a prefix of - ``skip_``. \ No newline at end of file + ``skip_``. + + +.. _using nox: + +Using Nox for Testing Iris +========================== + +Iris has adopted the use of the `nox`_ tool for automated testing on `cirrus-ci`_ +and also locally on the command-line for developers. + +`nox`_ is similar to `tox`_, but instead leverages the expressiveness and power of a Python +configuration file rather than an `.ini` style file. As with `tox`_, `nox`_ can use `virtualenv`_ +to create isolated Python environments, but in addition also supports `conda`_ as a testing +environment backend. + + +Where is Nox Used? +------------------ + +Iris uses `nox`_ as a convenience to fully automate the process of executing the Iris tests, but also +automates the process of: + +* building the documentation and executing the doc-tests +* building the documentation gallery +* running the documentation URL link check +* linting the code-base +* ensuring the code-base style conforms to the `black`_ standard + + +You can perform all of these tasks manually yourself, however the onus is on you to first ensure +that all of the required package dependencies are installed and available in the testing environment. + +`Nox`_ has been configured to automatically do this for you, and provides a means to easily replicate +the remote testing behaviour of `cirrus-ci`_ locally for the developer. + + +Installing Nox +-------------- + +We recommend installing `nox`_ using `conda`_. To install `nox`_ in a separate `conda`_ environment:: + + conda create -n nox -c conda-forge nox + conda activate nox + +To install `nox`_ in an existing active `conda`_ environment:: + + conda install -c conda-forge nox + +The `nox`_ package is also available on PyPI, however `nox`_ has been configured to use the `conda`_ +backend for Iris, so an installation of `conda`_ must always be available. + + +Testing with Nox +---------------- + +The `nox`_ configuration file `noxfile.py` is available in the root ``iris`` project directory, and +defines all the `nox`_ sessions (i.e., tasks) that may be performed. `nox`_ must always be executed +from the ``iris`` root directory. + +To list the configured `nox`_ sessions for Iris:: + + nox --list + +To run the Iris tests for all configured versions of Python:: + + nox --session tests + +To build the Iris documentation specifically for Python 3.7:: + + nox --session doctest-3.7 + +To run all the Iris `nox`_ sessions:: + + nox + +For further `nox`_ command-line options:: + + nox --help + +.. note:: `nox`_ will cache its testing environments in the `.nox` root ``iris`` project directory. + + +.. _black: https://black.readthedocs.io/en/stable/ +.. _nox: https://nox.thea.codes/en/latest/ +.. _setuptools: https://setuptools.readthedocs.io/en/latest/ +.. _tox: https://tox.readthedocs.io/en/latest/ +.. _virtualenv: https://virtualenv.pypa.io/en/latest/ +.. _PyPI: https://pypi.org/project/nox/ +.. _v41.5.0: https://setuptools.readthedocs.io/en/latest/history.html#v41-5-0 diff --git a/docs/iris/src/developers_guide/contributing_testing.rst b/docs/iris/src/developers_guide/contributing_testing.rst index 375ad570031..486af706d3c 100644 --- a/docs/iris/src/developers_guide/contributing_testing.rst +++ b/docs/iris/src/developers_guide/contributing_testing.rst @@ -3,7 +3,7 @@ .. _developer_test_categories: -Test categories +Test Categories *************** There are two main categories of tests within Iris: @@ -20,7 +20,7 @@ feel free to submit a pull-request in any state and ask for assistance. .. _testing.unit_test: -Unit tests +Unit Tests ========== Code changes should be accompanied by enough unit tests to give a @@ -128,7 +128,7 @@ Within that file the tests might look something like: .. _testing.integration: -Integration tests +Integration Tests ================= Some code changes may require tests which exercise several units in @@ -141,4 +141,4 @@ tests. But folders and files must be created as required to help developers locate relevant tests. It is recommended they are named according to the capabilities under test, e.g. ``metadata/test_pp_preservation.py``, and not named according to the -module(s) under test. \ No newline at end of file +module(s) under test. diff --git a/docs/iris/src/developers_guide/documenting/docstrings.rst b/docs/iris/src/developers_guide/documenting/docstrings.rst index 34ec790d033..8a06024ee23 100644 --- a/docs/iris/src/developers_guide/documenting/docstrings.rst +++ b/docs/iris/src/developers_guide/documenting/docstrings.rst @@ -27,7 +27,7 @@ There are two forms of docstrings: **single-line** and **multi-line** docstrings. -Single-line docstrings +Single-Line Docstrings ====================== The single line docstring of an object must state the **purpose** of that @@ -35,7 +35,7 @@ object, known as the **purpose section**. This terse overview must be on one line and ideally no longer than 80 characters. -Multi-line docstrings +Multi-Line Docstrings ===================== Multi-line docstrings must consist of at least a purpose section akin to the @@ -53,7 +53,7 @@ not to document *argument* and *keyword argument* details. Such information should be documented in the following *arguments and keywords section*. -Sample multi-line docstring +Sample Multi-Line Docstring --------------------------- Here is a simple example of a standard docstring: @@ -75,7 +75,7 @@ Additionally, a summary can be extracted automatically, which would result in: documenting.docstrings_sample_routine.sample_routine -Documenting classes +Documenting Classes =================== The class constructor should be documented in the docstring for its @@ -90,7 +90,7 @@ superclass method and does not call the superclass method; use the verb (in addition to its own behaviour). -Attribute and property docstrings +Attribute and Property Docstrings --------------------------------- Here is a simple example of a class containing an attribute docstring and a diff --git a/docs/iris/src/developers_guide/documenting/rest_guide.rst b/docs/iris/src/developers_guide/documenting/rest_guide.rst index bc34d16cd8d..4845132b159 100644 --- a/docs/iris/src/developers_guide/documenting/rest_guide.rst +++ b/docs/iris/src/developers_guide/documenting/rest_guide.rst @@ -3,7 +3,7 @@ .. _reST_quick_start: ================ -reST quick start +reST Quick Start ================ `reST`_ is used to create the documentation for Iris_. It is used to author @@ -19,7 +19,7 @@ reST markup syntaxes, for the basics of reST the following links may be useful: Reference documentation for reST can be found at http://docutils.sourceforge.net/rst.html. -Creating links +Creating Links -------------- Basic links can be created with ```Text of the link `_`` which will look like `Text of the link `_ diff --git a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst index 856d9af0a9a..d6f805c5112 100644 --- a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst @@ -1,7 +1,7 @@ .. _whats_new_contributions: ================================= -Contributing a "What's New" entry +Contributing a "What's New" Entry ================================= Iris uses a file named ``latest.rst`` to keep a draft of upcoming changes @@ -38,7 +38,7 @@ situation is thought likely (large PR, high repo activity etc.): * PR author: create the "What's New" pull request * PR reviewer: once the "What's New" PR is created, **merge the main PR**. - (this will fix any `travis-ci`_ linkcheck errors where the links in the + (this will fix any `cirrus-ci`_ linkcheck errors where the links in the "What's New" PR reference new features introduced in the main PR) * PR reviewer: review the "What's New" PR, merge once acceptable @@ -48,7 +48,7 @@ for the minimum time, minimising conflicts and minimising the need to rebase or merge from trunk. -Writing a contribution +Writing a Contribution ====================== As introduced above, a contribution is the description of a change to Iris @@ -96,14 +96,14 @@ examine past what's :ref:`iris_whatsnew` entries. .. note:: The reStructuredText syntax will be checked as part of building the documentation. Any warnings should be corrected. - `travis-ci`_ will automatically build the documentation when + `cirrus-ci`_ will automatically build the documentation when creating a pull request, however you can also manually :ref:`build ` the documentation. -.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris -Contribution categories +Contribution Categories ======================= The structure of the what's new release note should be easy to read by diff --git a/docs/iris/src/developers_guide/gitwash/configure_git.rst b/docs/iris/src/developers_guide/gitwash/configure_git.rst index b958a683ee2..6fc288daf99 100644 --- a/docs/iris/src/developers_guide/gitwash/configure_git.rst +++ b/docs/iris/src/developers_guide/gitwash/configure_git.rst @@ -3,7 +3,7 @@ .. _configure-git: ============= -Configure git +Configure Git ============= .. _git-config-basic: @@ -51,7 +51,7 @@ command:: To set up on another computer, you can copy your ``~/.gitconfig`` file, or run the commands above. -In detail +In Detail ========= user.name and user.email @@ -124,7 +124,7 @@ Or from the command line:: .. _fancy-log: -Fancy log output +Fancy Log Output ---------------- This is a very nice alias to get a fancy log output; it should go in the diff --git a/docs/iris/src/developers_guide/gitwash/development_workflow.rst b/docs/iris/src/developers_guide/gitwash/development_workflow.rst index b67885e6bd6..f6144a05e97 100644 --- a/docs/iris/src/developers_guide/gitwash/development_workflow.rst +++ b/docs/iris/src/developers_guide/gitwash/development_workflow.rst @@ -1,14 +1,14 @@ .. _development-workflow: #################### -Development workflow +Development Workflow #################### You already have your own forked copy of the `iris`_ repository, by following :ref:`forking`. You have :ref:`set-up-fork`. You have configured git by following :ref:`configure-git`. Now you are ready for some real work. -Workflow summary +Workflow Summary ================ In what follows we'll refer to the upstream iris ``master`` branch, as @@ -34,7 +34,7 @@ what you've done, and why you did it. See `linux git workflow`_ for some explanation. -Consider deleting your master branch +Consider Deleting Your Master Branch ==================================== It may sound strange, but deleting your own ``master`` branch can help reduce @@ -43,7 +43,7 @@ details. .. _update-mirror-trunk: -Update the mirror of trunk +Update the Mirror of Trunk ========================== First make sure you have done :ref:`linking-to-upstream`. @@ -59,7 +59,7 @@ you last checked, ``upstream/master`` will change after you do the fetch. .. _make-feature-branch: -Make a new feature branch +Make a New Feature Branch ========================= When you are ready to make some changes to the code, you should start a new @@ -99,7 +99,7 @@ From now on git will know that ``my-new-feature`` is related to the .. _edit-flow: -The editing workflow +The Editing Workflow ==================== Overview @@ -112,7 +112,7 @@ Overview git commit -am 'NF - some message' git push -In more detail +In More Detail -------------- #. Make some changes @@ -144,14 +144,14 @@ In more detail push`` (see `git push`_). -Testing your changes +Testing Your Changes ==================== Once you are happy with your changes, work thorough the :ref:`pr_check` and make sure your branch passes all the relevant tests. -Ask for your changes to be reviewed or merged +Ask for Your Changes to be Reviewed or Merged ============================================= When you are ready to ask for someone to review your code and consider a merge: @@ -175,10 +175,10 @@ When you are ready to ask for someone to review your code and consider a merge: pull request message. This is still a good way of getting some preliminary code review. -Some other things you might want to do +Some Other Things you Might Want to do ====================================== -Delete a branch on github +Delete a Branch on Github ------------------------- :: @@ -193,7 +193,7 @@ Note the colon ``:`` before ``test-branch``. See also: http://github.com/guides/remove-a-remote-branch -Several people sharing a single repository +Several People Sharing a Single Repository ------------------------------------------ If you want to work on some stuff with other people, where you are all @@ -225,7 +225,7 @@ usual:: git commit -am 'ENH - much better code' git push origin master # pushes directly into your repo -Explore your repository +Explore Your Repository ----------------------- To see a graphical representation of the repository branches and @@ -243,7 +243,7 @@ graph of the repository. .. _rebase-on-trunk: -Rebasing on trunk +Rebasing on Trunk ----------------- For more information please see the diff --git a/docs/iris/src/developers_guide/gitwash/forking.rst b/docs/iris/src/developers_guide/gitwash/forking.rst index e10b8f84ca1..161847ed793 100644 --- a/docs/iris/src/developers_guide/gitwash/forking.rst +++ b/docs/iris/src/developers_guide/gitwash/forking.rst @@ -3,7 +3,7 @@ .. _forking: =================================== -Making your own copy (fork) of Iris +Making Your own Copy (fork) of Iris =================================== You need to do this only once. The instructions here are very similar @@ -12,7 +12,7 @@ that page for more detail. We're repeating some of it here just to give the specifics for the `Iris`_ project, and to suggest some default names. -Set up and configure a github account +Set up and Configure a Github Account ===================================== If you don't have a github account, go to the github page, and make one. @@ -21,7 +21,7 @@ You then need to configure your account to allow write access, see the `generating sss keys for GitHub`_ help on `github help`_. -Create your own forked copy of Iris +Create Your own Forked Copy of Iris =================================== #. Log into your github account. diff --git a/docs/iris/src/developers_guide/gitwash/index.rst b/docs/iris/src/developers_guide/gitwash/index.rst index d0e70597f1b..3cde6225831 100644 --- a/docs/iris/src/developers_guide/gitwash/index.rst +++ b/docs/iris/src/developers_guide/gitwash/index.rst @@ -1,6 +1,6 @@ .. _using-git: -Working with Iris source code +Working With Iris Source Code ============================= .. toctree:: diff --git a/docs/iris/src/developers_guide/gitwash/set_up_fork.rst b/docs/iris/src/developers_guide/gitwash/set_up_fork.rst index 9dc6618c64f..70d602c97c3 100644 --- a/docs/iris/src/developers_guide/gitwash/set_up_fork.rst +++ b/docs/iris/src/developers_guide/gitwash/set_up_fork.rst @@ -3,7 +3,7 @@ .. _set-up-fork: ================ -Set up your fork +Set up Your Fork ================ First you follow the instructions for :ref:`forking`. @@ -17,10 +17,10 @@ Overview cd iris git remote add upstream git://github.com/SciTools/iris.git -In detail +In Detail ========= -Clone your fork +Clone Your Fork --------------- #. Clone your fork to the local computer with ``git clone @@ -42,7 +42,7 @@ Clone your fork .. _linking-to-upstream: -Linking your repository to the upstream repo +Linking Your Repository to the Upstream Repo -------------------------------------------- :: diff --git a/docs/iris/src/developers_guide/release.rst b/docs/iris/src/developers_guide/release.rst index 2ec787a7806..6ac3af5c75e 100644 --- a/docs/iris/src/developers_guide/release.rst +++ b/docs/iris/src/developers_guide/release.rst @@ -10,7 +10,7 @@ The summary below is of the main areas that constitute the release. The final section details the :ref:`iris_development_releases_steps` to take. -Before release +Before Release -------------- Deprecations @@ -21,7 +21,7 @@ previous releases is now finally changed. More detail, including the correct number of releases, is in :ref:`iris_development_deprecations`. -Release branch +Release Branch -------------- Once the features intended for the release are on master, a release branch @@ -37,7 +37,7 @@ This branch shall be used to finalise the release details in preparation for the release candidate. -Release candidate +Release Candidate ----------------- Prior to a release, a release candidate tag may be created, marked as a @@ -67,7 +67,7 @@ This content should be reviewed and adapted as required. Steps to achieve this can be found in the :ref:`iris_development_releases_steps`. -The release +The Release ----------- The final steps are to change the version string in the source of @@ -78,7 +78,7 @@ Once all checks are complete, the release is cut by the creation of a new tag in the SciTools Iris repository. -Conda recipe +Conda Recipe ------------ Once a release is cut, the `Iris feedstock`_ for the conda recipe must be @@ -88,7 +88,7 @@ updated to build the latest release of Iris and push this artefact to .. _Iris feedstock: https://github.com/conda-forge/iris-feedstock/tree/master/recipe .. _conda forge: https://anaconda.org/conda-forge/iris -Merge back +Merge Back ---------- After the release is cut, the changes shall be merged back onto the @@ -101,7 +101,7 @@ pull request to master. This work flow ensures that the commit identifiers are consistent between the :literal:`.x` branch and :literal:`master`. -Point releases +Point Releases -------------- Bug fixes may be implemented and targeted as the :literal:`.x` branch. These @@ -118,12 +118,12 @@ release process is to be followed, including the merge back of changes into .. _iris_development_releases_steps: -Maintainer steps +Maintainer Steps ---------------- These steps assume a release for ``v1.9`` is to be created -Release steps +Release Steps ~~~~~~~~~~~~~ #. Create the branch ``1.9.x`` on the main repo, not in a forked repo, for the @@ -156,7 +156,7 @@ Release steps `Iris release page `_ -Post release steps +Post Release Steps ~~~~~~~~~~~~~~~~~~ #. Check the documentation has built on `Read The Docs`_. The build is diff --git a/docs/iris/src/further_topics/index.rst b/docs/iris/src/further_topics/index.rst index 8a4d95b6cd1..dc162d6a1e2 100644 --- a/docs/iris/src/further_topics/index.rst +++ b/docs/iris/src/further_topics/index.rst @@ -5,7 +5,7 @@ Introduction Some specific areas of Iris may require further explanation or a deep dive into additional detail above and beyond that offered by the -:ref:`User guide `. +:ref:`User Guide `. This section provides a collection of additional material on focused topics that may be of interest to the more advanced or curious user. diff --git a/docs/iris/src/further_topics/lenient_maths.rst b/docs/iris/src/further_topics/lenient_maths.rst index 6f139fd9bf2..4aad721780d 100644 --- a/docs/iris/src/further_topics/lenient_maths.rst +++ b/docs/iris/src/further_topics/lenient_maths.rst @@ -1,6 +1,6 @@ .. _lenient maths: -Lenient cube maths +Lenient Cube Maths ****************** This section provides an overview of lenient cube maths. In particular, it explains @@ -46,7 +46,7 @@ a practical worked example, which we'll explore together next. .. _lenient example: -Lenient example +Lenient Example =============== .. testsetup:: lenient-example @@ -154,7 +154,7 @@ Now let's compare and contrast this lenient result with the strict alternative. But before we do so, let's first clarify how to control the behaviour of cube maths. -Control the behaviour +Control the Behaviour ===================== As stated earlier, lenient cube maths is the default behaviour from Iris ``3.0.0``. @@ -191,7 +191,7 @@ scope of the ``LENIENT`` `context manager`_, Lenient(maths=True) -Strict example +Strict Example ============== Now that we know how to control the underlying behaviour of cube maths, @@ -229,7 +229,7 @@ This is because strict cube maths, in general, will only return common metadata and common coordinates that are :ref:`strictly equivalent `. -Finer detail +Finer Detail ============ In general, if you want to preserve as much metadata and coordinate information as @@ -278,4 +278,4 @@ resultant :class:`~iris.cube.Cube`, .. _atmosphere hybrid height parametric vertical coordinate: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#atmosphere-hybrid-height-coordinate -.. _context manager: https://docs.python.org/3/library/contextlib.html \ No newline at end of file +.. _context manager: https://docs.python.org/3/library/contextlib.html diff --git a/docs/iris/src/further_topics/lenient_metadata.rst b/docs/iris/src/further_topics/lenient_metadata.rst index ada70497863..b68ed501ba7 100644 --- a/docs/iris/src/further_topics/lenient_metadata.rst +++ b/docs/iris/src/further_topics/lenient_metadata.rst @@ -1,6 +1,6 @@ .. _lenient metadata: -Lenient metadata +Lenient Metadata **************** This section discusses lenient metadata; what it is, what it means, and how you @@ -27,7 +27,7 @@ methods that provide this rich metadata behaviour, all of which are explored more fully in :ref:`metadata`. -Strict behaviour +Strict Behaviour ================ .. testsetup:: strict-behaviour @@ -137,7 +137,7 @@ practical behaviour is available. .. _lenient behaviour: -Lenient behaviour +Lenient Behaviour ================= .. testsetup:: lenient-behaviour @@ -210,7 +210,7 @@ lenient behaviour for each of the metadata classes. .. _lenient equality: -Lenient equality +Lenient Equality ---------------- Lenient equality is enabled using the ``lenient`` keyword argument, therefore @@ -273,7 +273,7 @@ forgiving and practical alternative to strict behaviour. .. _lenient difference: -Lenient difference +Lenient Difference ------------------ Similar to :ref:`lenient equality`, the lenient ``difference`` method @@ -330,7 +330,7 @@ highlights the change in how such dissimilar metadata is treated gracefully, .. _lenient combination: -Lenient combination +Lenient Combination ------------------- The behaviour of the lenient ``combine`` metadata class method is outlined @@ -380,7 +380,7 @@ for more inclusive, richer metadata, .. _lenient members: -Lenient members +Lenient Members --------------- :ref:`lenient behaviour` is not applied regardlessly across all metadata members @@ -429,7 +429,7 @@ strict behaviour, regardlessly. .. _special lenient name: -Special lenient name behaviour +Special Lenient Name Behaviour ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The ``standard_name``, ``long_name`` and ``var_name`` have a closer association diff --git a/docs/iris/src/further_topics/metadata.rst b/docs/iris/src/further_topics/metadata.rst index 3536c87a2bb..e6d6ebc57a8 100644 --- a/docs/iris/src/further_topics/metadata.rst +++ b/docs/iris/src/further_topics/metadata.rst @@ -42,7 +42,7 @@ Collectively, the aforementioned classes will be known here as the Iris `SciTools/iris`_ -Common metadata +Common Metadata =============== Each of the Iris `CF Conventions`_ classes use **metadata** to define them and @@ -69,7 +69,7 @@ actual `data attribute`_ names of the metadata members on the Iris class. :align: center =================== ======================================= ============================== ========================================== ================================= ======================== ============================== =================== - Metadata members :class:`~iris.coords.AncillaryVariable` :class:`~iris.coords.AuxCoord` :class:`~iris.aux_factory.AuxCoordFactory` :class:`~iris.coords.CellMeasure` :class:`~iris.cube.Cube` :class:`~iris.coords.DimCoord` Metadata members + Metadata Members :class:`~iris.coords.AncillaryVariable` :class:`~iris.coords.AuxCoord` :class:`~iris.aux_factory.AuxCoordFactory` :class:`~iris.coords.CellMeasure` :class:`~iris.cube.Cube` :class:`~iris.coords.DimCoord` Metadata Members =================== ======================================= ============================== ========================================== ================================= ======================== ============================== =================== ``standard_name`` ✔ ✔ ✔ ✔ ✔ ✔ ``standard_name`` ``long_name`` ✔ ✔ ✔ ✔ ✔ ✔ ``long_name`` @@ -90,7 +90,7 @@ actual `data attribute`_ names of the metadata members on the Iris class. terms. -Common metadata API +Common Metadata API =================== .. testsetup:: @@ -149,7 +149,7 @@ a **common** and **consistent** approach to managing your metadata, which we'll now explore a little more fully. -Metadata classes +Metadata Classes ---------------- The ``metadata`` property will return an appropriate `namedtuple`_ metadata class @@ -162,7 +162,7 @@ each container class is shown in :numref:`metadata classes table` below, :align: center ========================================== ======================================================== - Container class Metadata class + Container Class Metadata Class ========================================== ======================================================== :class:`~iris.coords.AncillaryVariable` :class:`~iris.common.metadata.AncillaryVariableMetadata` :class:`~iris.coords.AuxCoord` :class:`~iris.common.metadata.CoordMetadata` @@ -232,7 +232,7 @@ discussion on options how to **set** and **get** metadata on the instance of an Iris `CF Conventions`_ container class (:numref:`metadata classes table`). -Metadata class behaviour +Metadata Class Behaviour ------------------------ As mentioned previously, the metadata classes in :numref:`metadata classes table` @@ -301,7 +301,7 @@ which we explore next. .. _richer metadata: -Richer metadata behaviour +Richer Metadata Behaviour ------------------------- .. testsetup:: richer-metadata @@ -320,7 +320,7 @@ allows you to easily **compare**, **combine**, **convert** and understand the .. _metadata equality: -Metadata equality +Metadata Equality ^^^^^^^^^^^^^^^^^ The metadata classes support both **equality** (``__eq__``) and **inequality** @@ -357,7 +357,7 @@ a means to enable **lenient** equality, as discussed in :ref:`lenient equality`. .. _strict equality: -Strict equality +Strict Equality """"""""""""""" By default, metadata class equality will perform a **strict** comparison between @@ -426,7 +426,7 @@ However, metadata class equality is rich enough to handle this eventuality, .. _compare like: -Comparing like with like +Comparing Like With Like """""""""""""""""""""""" So far in our journey through metadata class equality, we have only considered @@ -446,7 +446,7 @@ metadata class contains **different** members, as shown in .. _exception rule: -Exception to the rule +Exception to the Rule ~~~~~~~~~~~~~~~~~~~~~ In general, **different** metadata classes cannot be compared, however support @@ -502,7 +502,7 @@ methods of metadata classes. .. _metadata difference: -Metadata difference +Metadata Difference ^^^^^^^^^^^^^^^^^^^ Being able to compare metadata is valuable, especially when we have the @@ -605,7 +605,7 @@ Now, let's compare the two above instances and see what ``attributes`` member di .. _diff like: -Diffing like with like +Diffing Like With Like """""""""""""""""""""" As discussed in :ref:`compare like`, it only makes sense to determine the @@ -655,7 +655,7 @@ In general, however, comparing **different** metadata classes will result in a .. _metadata combine: -Metadata combination +Metadata Combination ^^^^^^^^^^^^^^^^^^^^ .. testsetup:: metadata-combine @@ -740,7 +740,7 @@ metadata class. This is explored in a little further detail next. .. _combine like: -Combine like with like +Combine Like With Like """""""""""""""""""""" Akin to the :ref:`equal ` and @@ -788,7 +788,7 @@ However, note that commutativity in this case cannot be honoured, for obvious re .. _metadata conversion: -Metadata conversion +Metadata Conversion ^^^^^^^^^^^^^^^^^^^ .. testsetup:: metadata-convert @@ -853,7 +853,7 @@ class instance, .. _metadata assignment: -Metadata assignment +Metadata Assignment ^^^^^^^^^^^^^^^^^^^ .. testsetup:: metadata-assign @@ -888,7 +888,7 @@ coordinate, DimCoordMetadata(standard_name='latitude', long_name=None, var_name='latitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False) -Assign by iterable +Assign by Iterable """""""""""""""""" It is also possible to assign to the ``metadata`` property of an Iris @@ -903,7 +903,7 @@ number** of associated member values, e.g., DimCoordMetadata(standard_name='latitude', long_name=None, var_name='latitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False) -Assign by namedtuple +Assign by Namedtuple """""""""""""""""""" A `namedtuple`_ may also be used to assign to the ``metadata`` property of an @@ -933,7 +933,7 @@ of the ``longitude`` coordinate, DimCoordMetadata(standard_name='latitude', long_name=None, var_name='latitude', units=Unit('degrees'), attributes={}, coord_system=GeogCS(6371229.0), climatological=False, circular=False) -Assign by mapping +Assign by Mapping """"""""""""""""" It is also possible to assign to the ``metadata`` property using a `mapping`_, diff --git a/docs/iris/src/index.rst b/docs/iris/src/index.rst index f230e36f755..80aa696ba10 100644 --- a/docs/iris/src/index.rst +++ b/docs/iris/src/index.rst @@ -46,7 +46,7 @@ For **Iris 2.4** and earlier documentation please see the :container: container-lg pb-3 :column: col-lg-4 col-md-4 col-sm-6 col-xs-12 p-2 - Install Iris to use or for development. + Install Iris as a user or developer. +++ .. link-button:: installing_iris :type: ref @@ -91,7 +91,7 @@ For **Iris 2.4** and earlier documentation please see the .. toctree:: :maxdepth: 1 - :caption: Getting started + :caption: Getting Started :hidden: installing diff --git a/docs/iris/src/installing.rst b/docs/iris/src/installing.rst index 5a81e59c88c..cd6a648516e 100644 --- a/docs/iris/src/installing.rst +++ b/docs/iris/src/installing.rst @@ -22,7 +22,7 @@ any WSL_ distributions. .. _installing_using_conda: -Installing using conda (users) +Installing Using Conda (Users) ------------------------------ To install Iris using conda, you must first download and install conda, @@ -78,8 +78,8 @@ dependency conflicts might arise or the procedure might have to modified. .. _installing_from_source: -Installing from source with conda (devs) ----------------------------------------- +Installing from Source with Conda (Developers) +---------------------------------------------- The latest Iris source release is available from https://github.com/SciTools/iris. @@ -115,7 +115,7 @@ to find your local Iris code:: python setup.py develop -Running the tests +Running the Tests ----------------- To ensure your setup is configured correctly you can run the test suite using @@ -126,7 +126,7 @@ the command:: For more information see :ref:`developer_running_tests`. -Custom site configuration +Custom Site Configuration ------------------------- The default site configuration values can be overridden by creating the file diff --git a/docs/iris/src/techpapers/change_management.rst b/docs/iris/src/techpapers/change_management.rst index ab45fe79263..f39d64f430b 100644 --- a/docs/iris/src/techpapers/change_management.rst +++ b/docs/iris/src/techpapers/change_management.rst @@ -4,7 +4,7 @@ .. _change_management: -Change Management in Iris from the User's perspective +Change Management in Iris From the User's Perspective ***************************************************** As Iris changes, user code will need revising from time to time to keep it @@ -16,7 +16,7 @@ Here, we define ways to make this as easy as possible. .. include:: ../userguide/change_management_goals.txt -Key principles you can rely on +Key Principles you can Rely on ============================== Iris code editions are published as defined version releases, with a given @@ -42,7 +42,7 @@ If your code produces :ref:`deprecation warnings `, then it -User Actions : How you should respond to changes and releases +User Actions : How you Should Respond to Changes and Releases ============================================================= Checklist : @@ -96,7 +96,7 @@ Key concepts covered here: .. _iris_backward_compatibility: -Backwards compatibility +Backwards Compatibility ----------------------- "Backwards-compatible" changes are those that leave any existing valid API @@ -135,7 +135,7 @@ See :ref:`Usage of iris.FUTURE `, below. .. _iris_api: -Terminology : API, features, usages and behaviours +Terminology : API, Features, Usages and Behaviours ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The API is the components of the iris module and its submodules which are @@ -320,7 +320,7 @@ This is to warn users : * eventually to rewrite old code to use the newer or better alternatives -Deprecated features support through the Release cycle +Deprecated Features Support Through the Release Cycle ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The whole point of a deprecation is that the feature continues to work, but @@ -341,7 +341,7 @@ follows: .. _iris_future_usage: -Future options, `iris.FUTURE` +Future Options, `iris.FUTURE` ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A special approach is needed where the replacement behaviour is not controlled diff --git a/docs/iris/src/techpapers/index.rst b/docs/iris/src/techpapers/index.rst index 3074569eae0..773c8f70598 100644 --- a/docs/iris/src/techpapers/index.rst +++ b/docs/iris/src/techpapers/index.rst @@ -1,7 +1,7 @@ .. _techpapers_index: -Iris technical papers +Iris Technical Papers ===================== Extra information on specific technical issues. diff --git a/docs/iris/src/techpapers/missing_data_handling.rst b/docs/iris/src/techpapers/missing_data_handling.rst index 46279bc5661..13b00d34245 100644 --- a/docs/iris/src/techpapers/missing_data_handling.rst +++ b/docs/iris/src/techpapers/missing_data_handling.rst @@ -1,5 +1,5 @@ ============================= -Missing data handling in Iris +Missing Data Handling in Iris ============================= This document provides a brief overview of how Iris handles missing data values @@ -73,7 +73,7 @@ all have the same fill-value. If the components have differing fill-values, a default fill-value will be used instead. -Other operations +Other Operations ---------------- Other operations, such as :class:`~iris.cube.Cube` arithmetic operations, diff --git a/docs/iris/src/techpapers/um_files_loading.rst b/docs/iris/src/techpapers/um_files_loading.rst index d8c796b31f6..72d34962ce7 100644 --- a/docs/iris/src/techpapers/um_files_loading.rst +++ b/docs/iris/src/techpapers/um_files_loading.rst @@ -14,7 +14,7 @@ =================================== -Iris handling of PP and Fieldsfiles +Iris Handling of PP and Fieldsfiles =================================== This document provides a basic account of how PP and Fieldsfiles data is @@ -40,7 +40,7 @@ For details of Iris terms (cubes, coordinates, attributes), refer to For details of CF conventions, see http://cfconventions.org/. -Overview of loading process +Overview of Loading Process --------------------------- The basics of Iris loading are explained at :ref:`loading_iris_cubes`. @@ -165,7 +165,7 @@ For example: sections are written only if the actual values are unevenly spaced. -Phenomenon identification +Phenomenon Identification ------------------------- **UM Field elements** @@ -218,7 +218,7 @@ For example: LBUSER4 and LBUSER7 elements. -Vertical coordinates +Vertical Coordinates -------------------- **UM Field elements** @@ -319,7 +319,7 @@ See an example printout of a hybrid height cube, .. _um_time_metadata: -Time information +Time Information ---------------- **UM Field elements** @@ -391,7 +391,7 @@ See an example printout of a forecast data cube, 'forecast_reference_time' is a constant. -Statistical measures +Statistical Measures -------------------- **UM Field elements** @@ -438,7 +438,7 @@ For example: (CellMethod(method='mean', coord_names=('time',), intervals=('6 hour',), comments=()),) -Other metadata +Other Metadata -------------- LBRSVD4 diff --git a/docs/iris/src/userguide/citation.rst b/docs/iris/src/userguide/citation.rst index f91bc670f08..0a3a85fb89c 100644 --- a/docs/iris/src/userguide/citation.rst +++ b/docs/iris/src/userguide/citation.rst @@ -8,7 +8,7 @@ If Iris played an important part in your research then please add us to your reference list by using one of the recommendations below. ************ -BibTeX entry +BibTeX Entry ************ For example:: @@ -24,7 +24,7 @@ For example:: ******************* -Downloaded software +Downloaded Software ******************* Suggested format:: @@ -37,7 +37,7 @@ For example:: ******************** -Checked out software +Checked Out Software ******************** Suggested format:: diff --git a/docs/iris/src/userguide/code_maintenance.rst b/docs/iris/src/userguide/code_maintenance.rst index d03808e18f5..b2b498bc80e 100644 --- a/docs/iris/src/userguide/code_maintenance.rst +++ b/docs/iris/src/userguide/code_maintenance.rst @@ -1,11 +1,11 @@ -Code maintenance +Code Maintenance ================ From a user point of view "code maintenance" means ensuring that your existing working code stays working, in the face of changes to Iris. -Stability and change +Stability and Change --------------------- In practice, as Iris develops, most users will want to periodically upgrade @@ -25,7 +25,7 @@ maintenance effort is probably still necessary: for some completely unconnected reason. -Principles of change management +Principles of Change Management ------------------------------- When you upgrade software to a new version, you often find that you need to diff --git a/docs/iris/src/userguide/cube_maths.rst b/docs/iris/src/userguide/cube_maths.rst index 1b1b2dbe662..d2d4d84b681 100644 --- a/docs/iris/src/userguide/cube_maths.rst +++ b/docs/iris/src/userguide/cube_maths.rst @@ -1,7 +1,7 @@ .. _cube maths: ========== -Cube maths +Cube Maths ========== @@ -29,7 +29,7 @@ In order to reduce the amount of metadata which becomes inconsistent, fundamental arithmetic operations such as addition, subtraction, division and multiplication can be applied directly to any cube. -Calculating the difference between two cubes +Calculating the Difference Between Two Cubes -------------------------------------------- Let's load some air temperature which runs from 1860 to 2100:: @@ -77,7 +77,7 @@ but with the data representing their difference: .. _cube-maths_anomaly: -Calculating a cube anomaly +Calculating a Cube Anomaly -------------------------- In section :doc:`cube_statistics` we discussed how the dimensionality of a cube @@ -165,7 +165,7 @@ broadcasting behaviour:: >>> print(result.summary(True)) unknown / (K) (time: 240; latitude: 37; longitude: 49) -Combining multiple phenomena to form a new one +Combining Multiple Phenomena to Form a New One ---------------------------------------------- Combining cubes of potential-temperature and pressure we can calculate @@ -223,7 +223,7 @@ The result could now be plotted using the guidance provided in the .. _cube_maths_combining_units: -Combining units +Combining Units --------------- It should be noted that when combining cubes by multiplication, division or diff --git a/docs/iris/src/userguide/cube_statistics.rst b/docs/iris/src/userguide/cube_statistics.rst index 310551c76f1..4eb016078e6 100644 --- a/docs/iris/src/userguide/cube_statistics.rst +++ b/docs/iris/src/userguide/cube_statistics.rst @@ -1,12 +1,12 @@ .. _cube-statistics: =============== -Cube statistics +Cube Statistics =============== .. _cube-statistics-collapsing: -Collapsing entire data dimensions +Collapsing Entire Data Dimensions --------------------------------- .. testsetup:: @@ -100,7 +100,7 @@ in the gallery takes a zonal mean of an ``XYT`` cube by using the .. _cube-statistics-collapsing-average: -Area averaging +Area Averaging ^^^^^^^^^^^^^^ Some operators support additional keywords to the ``cube.collapsed`` method. @@ -152,14 +152,14 @@ including an example on taking a :ref:`global area-weighted mean .. _cube-statistics-aggregated-by: -Partially reducing data dimensions +Partially Reducing Data Dimensions ---------------------------------- Instead of completely collapsing a dimension, other methods can be applied to reduce or filter the number of data points of a particular dimension. -Aggregation of grouped data +Aggregation of Grouped Data ^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :meth:`Cube.aggregated_by ` operation diff --git a/docs/iris/src/userguide/interpolation_and_regridding.rst b/docs/iris/src/userguide/interpolation_and_regridding.rst index ffed21a7f52..5a5a985ccb5 100644 --- a/docs/iris/src/userguide/interpolation_and_regridding.rst +++ b/docs/iris/src/userguide/interpolation_and_regridding.rst @@ -8,7 +8,7 @@ warnings.simplefilter('ignore') ================================= -Cube interpolation and regridding +Cube Interpolation and Regridding ================================= Iris provides powerful cube-aware interpolation and regridding functionality, @@ -123,7 +123,7 @@ will be orthogonal: air_temperature / (K) (latitude: 13; longitude: 14) -Interpolating non-horizontal coordinates +Interpolating Non-Horizontal Coordinates ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Interpolation in Iris is not limited to horizontal-spatial coordinates - any @@ -195,7 +195,7 @@ For example, to mask values that lie beyond the range of the original data: .. _caching_an_interpolator: -Caching an interpolator +Caching an Interpolator ^^^^^^^^^^^^^^^^^^^^^^^ If you need to interpolate a cube on multiple sets of sample points you can @@ -305,7 +305,7 @@ cells have now become rectangular in a plate carrée (equirectangular) projectio The spatial grid of the resulting cube is really global, with a large proportion of the data being masked. -Area-weighted regridding +Area-Weighted Regridding ^^^^^^^^^^^^^^^^^^^^^^^^ It is often the case that a point-based regridding scheme (such as @@ -384,7 +384,7 @@ To visualise the above regrid, let's plot the original data, along with 3 distin .. _caching_a_regridder: -Caching a regridder +Caching a Regridder ^^^^^^^^^^^^^^^^^^^ If you need to regrid multiple cubes with a common source grid onto a common @@ -415,7 +415,7 @@ In each case ``result`` will be the input cube regridded to the grid defined by the target grid cube (in this case ``rotated_psl``) that we used to define the cached regridder. -Regridding lazy data +Regridding Lazy Data ^^^^^^^^^^^^^^^^^^^^ If you are working with large cubes, especially when you are regridding to a diff --git a/docs/iris/src/userguide/iris_cubes.rst b/docs/iris/src/userguide/iris_cubes.rst index 5929c402f2f..de206486d3f 100644 --- a/docs/iris/src/userguide/iris_cubes.rst +++ b/docs/iris/src/userguide/iris_cubes.rst @@ -1,7 +1,7 @@ .. _iris_data_structures: ==================== -Iris data structures +Iris Data Structures ==================== The top level object in Iris is called a cube. A cube contains data and metadata about a phenomenon. @@ -71,11 +71,11 @@ A cube consists of: * a list of coordinate "factories" used for deriving coordinates from the values of other coordinates in the cube -Cubes in practice +Cubes in Practice ----------------- -A simple cube example +A Simple Cube Example ===================== Suppose we have some gridded data which has 24 air temperature readings (in Kelvin) which is located at @@ -137,7 +137,7 @@ For example, it is possible to attach any of the following: a collection of "ensembles" (i.e. multiple model runs). -Printing a cube +Printing a Cube =============== Every Iris cube can be printed to screen as you will see later in the user guide. It is worth familiarising yourself with the diff --git a/docs/iris/src/userguide/loading_iris_cubes.rst b/docs/iris/src/userguide/loading_iris_cubes.rst index 006a9194083..659c28420a6 100644 --- a/docs/iris/src/userguide/loading_iris_cubes.rst +++ b/docs/iris/src/userguide/loading_iris_cubes.rst @@ -1,7 +1,7 @@ .. _loading_iris_cubes: =================== -Loading Iris cubes +Loading Iris Cubes =================== To load a single file into a **list** of Iris cubes @@ -116,7 +116,7 @@ This was the output discussed at the end of the :doc:`iris_cubes` section. appropriate column for each cube data dimension that they describe. -Loading multiple files +Loading Multiple Files ----------------------- To load more than one file into a list of cubes, a list of filenames can be @@ -142,7 +142,7 @@ star wildcards can be used:: The cubes returned will not necessarily be in the same order as the order of the filenames. -Lazy loading +Lazy Loading ------------ In fact when Iris loads data from most file types, it normally only reads the @@ -155,7 +155,7 @@ For more on the benefits, handling and uses of lazy data, see :doc:`Real and Laz .. _constrained-loading: -Constrained loading +Constrained Loading ----------------------- Given a large dataset, it is possible to restrict or constrain the load to match specific Iris cube metadata. @@ -261,7 +261,7 @@ then specific STASH codes can be filtered:: :class:`iris.Constraint` reference documentation. -Constraining a circular coordinate across its boundary +Constraining a Circular Coordinate Across its Boundary ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Occasionally you may need to constrain your cube with a region that crosses the @@ -403,7 +403,7 @@ Notice how the dates printed are between the range specified in the ``st_swithun and that they span multiple years. -Strict loading +Strict Loading -------------- The :py:func:`iris.load_cube` and :py:func:`iris.load_cubes` functions are diff --git a/docs/iris/src/userguide/merge_and_concat.rst b/docs/iris/src/userguide/merge_and_concat.rst index 0d844ac403b..ffa36ccdebf 100644 --- a/docs/iris/src/userguide/merge_and_concat.rst +++ b/docs/iris/src/userguide/merge_and_concat.rst @@ -1,7 +1,7 @@ .. _merge_and_concat: ===================== -Merge and concatenate +Merge and Concatenate ===================== We saw in the :doc:`loading_iris_cubes` chapter that Iris tries to load as few cubes as @@ -203,7 +203,7 @@ single cube. An example of fixing an issue like this can be found in the :ref:`merge_concat_common_issues` section. -Merge in Iris load +Merge in Iris Load ================== The CubeList's :meth:`~iris.cube.CubeList.merge` method is used internally @@ -365,7 +365,7 @@ single cube. An example of fixing an issue like this can be found in the .. _merge_concat_common_issues: -Common issues with merge and concatenate +Common Issues With Merge and Concatenate ---------------------------------------- The Iris algorithms that drive :meth:`~iris.cube.CubeList.merge` and @@ -529,7 +529,7 @@ Trying to merge the input cubes with duplicate cubes not allowed raises an error highlighting the presence of the duplicate cube. -**Single value coordinates** +**Single Value Coordinates** Coordinates containing only a single value can cause confusion when combining input cubes. Remember: diff --git a/docs/iris/src/userguide/navigating_a_cube.rst b/docs/iris/src/userguide/navigating_a_cube.rst index a7b7717ae3c..df18c032c14 100644 --- a/docs/iris/src/userguide/navigating_a_cube.rst +++ b/docs/iris/src/userguide/navigating_a_cube.rst @@ -1,5 +1,5 @@ ================= -Navigating a cube +Navigating a Cube ================= .. testsetup:: @@ -15,7 +15,7 @@ Navigating a cube After loading any cube, you will want to investigate precisely what it contains. This section is all about accessing and manipulating the metadata contained within a cube. -Cube string representations +Cube String Representations --------------------------- We have already seen a basic string representation of a cube when printing: @@ -52,7 +52,7 @@ variable. In most cases it is reasonable to ignore anything starting with a "``_ dir(cube) help(cube) -Working with cubes +Working With Cubes ------------------ Every cube has a standard name, long name and units which are accessed with @@ -111,7 +111,7 @@ cube with the :attr:`Cube.cell_methods ` attribute: print(cube.cell_methods) -Accessing coordinates on the cube +Accessing Coordinates on the Cube --------------------------------- A cube's coordinates can be retrieved via :meth:`Cube.coords `. @@ -148,7 +148,7 @@ numpy array. If the coordinate has no bounds ``None`` will be returned:: print(type(coord.bounds)) -Adding metadata to a cube +Adding Metadata to a Cube ------------------------- We can add and remove coordinates via :func:`Cube.add_dim_coord`, @@ -177,7 +177,7 @@ We can add and remove coordinates via :func:`Cube.add_dim_coord`_ package in order to generate @@ -13,7 +13,7 @@ been extended within Iris to facilitate easy visualisation of a cube's data. *************************** -Matplotlib's pyplot basics +Matplotlib's Pyplot Basics *************************** A simple line plot can be created using the @@ -35,7 +35,7 @@ There are two modes of rendering within Matplotlib; **interactive** and **non-interactive**. -Interactive plot rendering +Interactive Plot Rendering ========================== The previous example was *non-interactive* as the figure is only rendered *after* the call to :py:func:`plt.show() `. @@ -84,7 +84,7 @@ so ensure that interactive mode is turned off with:: plt.interactive(False) -Saving a plot +Saving a Plot ============= The :py:func:`matplotlib.pyplot.savefig` function is similar to **plt.show()** @@ -113,7 +113,7 @@ Some of the formats which are supported by **plt.savefig**: ====== ====== ====================================================================== ****************** -Iris cube plotting +Iris Cube Plotting ****************** The Iris modules :py:mod:`iris.quickplot` and :py:mod:`iris.plot` extend the @@ -149,7 +149,7 @@ where appropriate. import iris.quickplot as qplt -Plotting 1-dimensional cubes +Plotting 1-Dimensional Cubes ============================ The simplest 1D plot is achieved with the :py:func:`iris.plot.plot` function. @@ -181,7 +181,7 @@ For example, the previous plot can be improved quickly by replacing -Multi-line plot +Multi-Line Plot --------------- A multi-lined (or over-plotted) plot, with a legend, can be achieved easily by @@ -212,10 +212,10 @@ the temperature at some latitude cross-sections. and run it using ``python my_file.py``. -Plotting 2-dimensional cubes +Plotting 2-Dimensional Cubes ============================ -Creating maps +Creating Maps ------------- Whenever a 2D plot is created using an :class:`iris.coord_systems.CoordSystem`, a cartopy :class:`~cartopy.mpl.GeoAxes` instance is created, which can be @@ -230,7 +230,7 @@ things. :meth:`cartopy's coastlines() `. -Cube contour +Cube Contour ------------ A simple contour plot of a cube can be created with either the :func:`iris.plot.contour` or :func:`iris.quickplot.contour` functions: @@ -239,7 +239,7 @@ A simple contour plot of a cube can be created with either the :include-source: -Cube filled contour +Cube Filled Contour ------------------- Similarly a filled contour plot of a cube can be created with the :func:`iris.plot.contourf` or :func:`iris.quickplot.contourf` functions: @@ -248,7 +248,7 @@ Similarly a filled contour plot of a cube can be created with the :include-source: -Cube block plot +Cube Block Plot --------------- In some situations the underlying coordinates are better represented with a continuous bounded coordinate, in which case a "block" plot may be more @@ -268,7 +268,7 @@ or :func:`iris.quickplot.pcolormesh`. .. _brewer-info: *********************** -Brewer colour palettes +Brewer Colour Palettes *********************** Iris includes colour specifications and designs developed by @@ -303,7 +303,7 @@ The following subset of Brewer palettes found at .. plot:: userguide/plotting_examples/brewer.py -Plotting with Brewer +Plotting With Brewer ==================== To plot a cube using a Brewer colour palette, simply select one of the Iris @@ -316,7 +316,7 @@ become available once :mod:`iris.plot` or :mod:`iris.quickplot` are imported. .. _brewer-cite: -Adding a citation +Adding a Citation ================= Citations can be easily added to a plot using the diff --git a/docs/iris/src/userguide/real_and_lazy_data.rst b/docs/iris/src/userguide/real_and_lazy_data.rst index 574ca4e1a0e..0bc18464579 100644 --- a/docs/iris/src/userguide/real_and_lazy_data.rst +++ b/docs/iris/src/userguide/real_and_lazy_data.rst @@ -10,7 +10,7 @@ ================== -Real and lazy data +Real and Lazy Data ================== We have seen in the :doc:`iris_cubes` section of the user guide that @@ -21,7 +21,7 @@ In this section of the user guide we will look specifically at the concepts of real and lazy data as they apply to the cube and other data structures in Iris. -What is real and lazy data? +What is Real and Lazy Data? --------------------------- In Iris, we use the term **real data** to describe data arrays that are loaded @@ -97,7 +97,7 @@ In such cases, a required portion can be extracted and realised without calculat .. _when_real_data: -When does my data become real? +When Does My Data Become Real? ------------------------------ Certain operations, such as cube indexing and statistics, can be @@ -134,7 +134,7 @@ You can also realise (and so load into memory) your cube's lazy data if you 'tou To 'touch' the data means directly accessing the data by calling ``cube.data``, as in the previous example. -Core data +Core Data ^^^^^^^^^ Cubes have the concept of "core data". This returns the cube's data in its @@ -225,7 +225,7 @@ coordinates' lazy points and bounds: Printing a lazy :class:`~iris.coords.AuxCoord` will realise its points and bounds arrays! -Dask processing options +Dask Processing Options ----------------------- Iris uses dask to provide lazy data arrays for both Iris cubes and coordinates, diff --git a/docs/iris/src/userguide/saving_iris_cubes.rst b/docs/iris/src/userguide/saving_iris_cubes.rst index 3a30321979c..237ceb18b65 100644 --- a/docs/iris/src/userguide/saving_iris_cubes.rst +++ b/docs/iris/src/userguide/saving_iris_cubes.rst @@ -1,7 +1,7 @@ .. _saving_iris_cubes: ================== -Saving Iris cubes +Saving Iris Cubes ================== Iris supports the saving of cubes and cube lists to: @@ -39,8 +39,8 @@ and the keyword argument `saver` is not required. attempting to overwrite an existing file. -Controlling the save process ------------------------------ +Controlling the Save Process +---------------------------- The :py:func:`iris.save` function passes all other keywords through to the saver function defined, or automatically set from the file extension. This enables saver specific functionality to be called. @@ -73,8 +73,8 @@ See for more details on supported arguments for the individual savers. -Customising the save process ------------------------------ +Customising the Save Process +---------------------------- When saving to GRIB or PP, the save process may be intercepted between the translation step and the file writing. This enables customisation of the output messages, based on Cube metadata if required, over and above the translations supplied by Iris. @@ -103,14 +103,14 @@ Similarly a PP field may need to be written out with a specific value for LBEXP. iris.fileformats.pp.save_fields(tweaked_fields(cubes[0]), '/tmp/app.pp') -netCDF -^^^^^^^ +NetCDF +^^^^^^ NetCDF is a flexible container for metadata and cube metadata is closely related to the CF for netCDF semantics. This means that cube metadata is well represented in netCDF files, closely resembling the in memory metadata representation. Thus there is no provision for similar save customisation functionality for netCDF saving, all customisations should be applied to the cube prior to saving to netCDF. -Bespoke saver --------------- +Bespoke Saver +------------- A bespoke saver may be written to support an alternative file format. This can be provided to the :py:func:`iris.save` function, enabling Iris to write to a different file format. Such a custom saver will need be written to meet the needs of the file format and to handle the metadata translation from cube metadata effectively. diff --git a/docs/iris/src/userguide/subsetting_a_cube.rst b/docs/iris/src/userguide/subsetting_a_cube.rst index 5d9a560be9d..02cf1645a11 100644 --- a/docs/iris/src/userguide/subsetting_a_cube.rst +++ b/docs/iris/src/userguide/subsetting_a_cube.rst @@ -1,7 +1,7 @@ .. _subsetting_a_cube: ================= -Subsetting a cube +Subsetting a Cube ================= The :doc:`loading_iris_cubes` section of the user guide showed how to load data into multidimensional Iris cubes. @@ -11,7 +11,7 @@ Iris provides several ways of reducing both the amount of data and/or the number In all cases **the subset of a valid cube is itself a valid cube**. -Cube extraction +Cube Extraction ^^^^^^^^^^^^^^^^ A subset of a cube can be "extracted" from a multi-dimensional cube in order to reduce its dimensionality: @@ -101,7 +101,7 @@ same way as loading with constraints: um_version: 7.3 -Cube iteration +Cube Iteration ^^^^^^^^^^^^^^^ It is not possible to directly iterate over an Iris cube. That is, you cannot use code such as ``for x in cube:``. However, you can iterate over cube slices, as this section details. @@ -152,7 +152,7 @@ slicing the 3 dimensional cube (15, 100, 100) by longitude (i starts at 0 and 15 cube using the slices method. -Cube indexing +Cube Indexing ^^^^^^^^^^^^^ In the same way that you would expect a numeric multidimensional array to be **indexed** to take a subset of your original array, you can **index** a Cube for the same purpose. diff --git a/docs/iris/src/whatsnew/1.0.rst b/docs/iris/src/whatsnew/1.0.rst index 11d29320b68..b226dc609b8 100644 --- a/docs/iris/src/whatsnew/1.0.rst +++ b/docs/iris/src/whatsnew/1.0.rst @@ -10,7 +10,7 @@ work. Following this release we plan to deliver significant performance improvements and additional features. -The role of 1.x +The Role of 1.x =============== The 1.x series of releases is intended to provide a relatively stable, @@ -58,7 +58,7 @@ A summary of the main features added with version 1.0: contain bounds. -CF-netCDF coordinate systems +CF-NetCDF Coordinate Systems ---------------------------- The coordinate systems in Iris are now defined by the CF-netCDF @@ -73,7 +73,7 @@ The coordinate systems available in Iris 1.0 and their corresponding Iris classes are: ================================================================================================================= ========================================= -CF name Iris class +CF Name Iris Class ================================================================================================================= ========================================= `Latitude-longitude `_ :class:`~iris.coord_systems.GeogCS` `Rotated pole `_ :class:`~iris.coord_systems.RotatedGeogCS` @@ -88,7 +88,7 @@ coordinate system used by the British .. _whats-new-cartopy: -Using Cartopy for mapping in matplotlib +Using Cartopy for Mapping in Matplotlib --------------------------------------- The underlying map drawing package has now been updated to use @@ -135,7 +135,7 @@ For more examples of what can be done with Cartopy, see the Iris gallery and `Cartopy's documentation `_. -Hybrid-pressure +Hybrid-Pressure --------------- With the introduction of the :class:`~iris.aux_factory.HybridPressureFactory` @@ -181,7 +181,7 @@ dealing with large numbers of netCDF files, or in long running processes. -Brewer colour palettes +Brewer Colour Palettes ---------------------- Iris includes a selection of carefully designed colour palettes produced @@ -207,7 +207,7 @@ To include a reference in a journal article or report please refer to in the citation guidance provided by Cynthia Brewer. -Metadata attributes +Metadata Attributes ------------------- Iris now stores "source" and "history" metadata in Cube attributes. @@ -241,7 +241,7 @@ Where previously it would have appeared as:: cube.add_aux_coord(src_coord) -New loading functions +New Loading Functions --------------------- The main functions for loading cubes are now: @@ -264,7 +264,7 @@ now use the :func:`iris.load_cube()` and :func:`iris.load_cubes()` functions instead. -Cube projection +Cube Projection --------------- Iris now has the ability to project a cube into a number of map projections. @@ -302,7 +302,7 @@ preserved. This function currently assumes global data and will if necessary extrapolate beyond the geographical extent of the source cube. -Incompatible changes +Incompatible Changes ==================== * The "source" and "history" metadata are now represented as Cube diff --git a/docs/iris/src/whatsnew/1.1.rst b/docs/iris/src/whatsnew/1.1.rst index f2b0995fa04..86f0bb16fa4 100644 --- a/docs/iris/src/whatsnew/1.1.rst +++ b/docs/iris/src/whatsnew/1.1.rst @@ -44,7 +44,7 @@ some notable improvements to netCDF/PP import. with product template 4.9. -Coordinate categorisation +Coordinate Categorisation ------------------------- An :func:`~iris.coord_categorisation.add_day_of_year` categorisation @@ -52,7 +52,7 @@ function has been added to the existing suite in :mod:`iris.coord_categorisation`. -Custom seasons +Custom Seasons ~~~~~~~~~~~~~~ The conventional seasonal categorisation functions have been @@ -87,7 +87,7 @@ This function adds a coordinate containing True/False values determined by membership of a single custom season. -Bugs fixed +Bugs Fixed ========== * PP export no longer attempts to set/overwrite the STASH code based on diff --git a/docs/iris/src/whatsnew/1.10.rst b/docs/iris/src/whatsnew/1.10.rst index 3f51287fa18..92822087dda 100644 --- a/docs/iris/src/whatsnew/1.10.rst +++ b/docs/iris/src/whatsnew/1.10.rst @@ -1,5 +1,5 @@ v1.10 (05 Sep 2016) -********************* +******************* This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -137,7 +137,7 @@ Features attributes is now allowed. -Bugs fixed +Bugs Fixed ========== * Altered Cell Methods to display coordinate's standard_name rather than @@ -215,7 +215,7 @@ Bugs fixed thrown while trying to subset over a non-dimensional scalar coordinate. -Incompatible changes +Incompatible Changes ==================== * The source and target for diff --git a/docs/iris/src/whatsnew/1.11.rst b/docs/iris/src/whatsnew/1.11.rst index e0d46d0f09f..356e6ec85b1 100644 --- a/docs/iris/src/whatsnew/1.11.rst +++ b/docs/iris/src/whatsnew/1.11.rst @@ -16,7 +16,7 @@ Features * The coordinate system :class:`iris.coord_systems.LambertAzimuthalEqualArea` has been added with NetCDF saving support. -Bugs fixed +Bugs Fixed ========== * Fixed a floating point tolerance bug in diff --git a/docs/iris/src/whatsnew/1.13.rst b/docs/iris/src/whatsnew/1.13.rst index 2d3b3ffce54..028c2985057 100644 --- a/docs/iris/src/whatsnew/1.13.rst +++ b/docs/iris/src/whatsnew/1.13.rst @@ -1,5 +1,5 @@ v1.13 (17 May 2017) -************************* +******************* This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -17,7 +17,7 @@ Features :meth:`iris.cube.share_data` flag. -Bug fixes +Bug Fixes ========= * The bounds are now set correctly on the longitude coordinate if a zonal mean diff --git a/docs/iris/src/whatsnew/1.2.rst b/docs/iris/src/whatsnew/1.2.rst index d4bb863a3b4..dce0b6dc042 100644 --- a/docs/iris/src/whatsnew/1.2.rst +++ b/docs/iris/src/whatsnew/1.2.rst @@ -44,7 +44,7 @@ Features :class:`~iris.cube.Cube`. -Bugs fixed +Bugs Fixed ========== * The GRIB hindcast interpretation of negative forecast times can be enabled @@ -54,7 +54,7 @@ Bugs fixed coordinates. -Incompatible changes +Incompatible Changes ==================== * The deprecated :attr:`iris.cube.Cube.unit` and :attr:`iris.coords.Coord.unit` diff --git a/docs/iris/src/whatsnew/1.3.rst b/docs/iris/src/whatsnew/1.3.rst index 9a2ac2eba1f..beaa594ab54 100644 --- a/docs/iris/src/whatsnew/1.3.rst +++ b/docs/iris/src/whatsnew/1.3.rst @@ -30,7 +30,7 @@ Features .. _whats-new-abf: -Loading ABF/ABL files +Loading ABF/ABL Files --------------------- Support for the ABF and ABL file formats (as @@ -51,7 +51,7 @@ For example:: .. _whats-new-cf-profile: -Customised CF profiles +Customised CF Profiles ---------------------- Iris now provides hooks in the CF-netCDF export process to allow @@ -74,7 +74,7 @@ For further implementation details see ``iris/fileformats/netcdf.py``. .. _whats-new-concat: -Cube concatenation +Cube Concatenation ------------------ Iris now provides initial support for concatenating Cubes along one or @@ -101,7 +101,7 @@ combine these into a single Cube as follows:: As this is an experimental feature, your feedback is especially welcome. -Bugs fixed +Bugs Fixed ========== * Printing a Cube now supports Unicode attribute values. @@ -123,7 +123,7 @@ Deprecations naming conventions. ====================================== =========================================== - Deprecated property/method New method + Deprecated Property/Method New Method ====================================== =========================================== :meth:`~iris.unit.Unit.convertible()` :meth:`~iris.unit.Unit.is_convertible()` :attr:`~iris.unit.Unit.dimensionless` :meth:`~iris.unit.Unit.is_dimensionless()` diff --git a/docs/iris/src/whatsnew/1.4.rst b/docs/iris/src/whatsnew/1.4.rst index 29f2079af8c..858f985ec6e 100644 --- a/docs/iris/src/whatsnew/1.4.rst +++ b/docs/iris/src/whatsnew/1.4.rst @@ -61,7 +61,7 @@ Features .. _OPeNDAP: http://www.opendap.org/about .. _exp-regrid: -Experimental regridding enhancements +Experimental Regridding Enhancements ------------------------------------ Bilinear, area-weighted and area-conservative regridding functions are now @@ -72,7 +72,7 @@ development. In the meantime: -Bilinear rectilinear regridding +Bilinear Rectilinear Regridding ------------------------------- :func:`~iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid` @@ -85,7 +85,7 @@ For example:: regridded_cube = regrid_bilinear_rectilinear_src_and_grid(source_cube, target_grid_cube) -Area-weighted regridding +Area-Weighted Regridding ------------------------ :func:`~iris.experimental.regrid.regrid_area_weighted_rectilinear_src_and_grid` @@ -98,7 +98,7 @@ For example:: regridded_cube = regrid_area_weighted(source_cube, target_grid_cube) -Area-conservative regridding +Area-Conservative Regridding ---------------------------- :func:`~iris.experimental.regrid_conservative.regrid_conservative_via_esmpy` @@ -113,7 +113,7 @@ For example:: .. _iris-pandas: -Iris-Pandas interoperability +Iris-Pandas Interoperability ---------------------------- Conversion to and from Pandas Series_ and DataFrames_ is now available. @@ -125,7 +125,7 @@ See :mod:`iris.pandas` for more details. .. _load-opendap: -Load cubes from the internet via OPeNDAP +Load Cubes From the Internet via OPeNDAP ---------------------------------------- Cubes can now be loaded directly from the internet, via OPeNDAP_. @@ -137,7 +137,7 @@ For example:: .. _geotiff_export: -GeoTiff export +GeoTiff Export -------------- With this experimental feature, two dimensional cubes can now be exported to @@ -155,7 +155,7 @@ For example:: .. _cube-merge-update: -Cube merge update +Cube Merge Update ----------------- Cube merging now favours numerical coordinates over string coordinates @@ -167,7 +167,7 @@ dimensions"*. .. _season-year-name: -Unambiguous season year naming +Unambiguous Season Year Naming ------------------------------ The default names of categorisation coordinates are now less ambiguous. @@ -178,7 +178,7 @@ For example, :func:`~iris.coord_categorisation.add_month_number` and .. _grib-novert: -Cubes with no vertical coord can now be exported to GRIB +Cubes With no Vertical Coord can now be Exported to GRIB -------------------------------------------------------- Iris can now export cubes with no vertical coord to GRIB. @@ -188,7 +188,7 @@ https://github.com/SciTools/iris/issues/519. .. _simple_cfg: -Simplified resource configuration +Simplified Resource Configuration --------------------------------- A new configuration variable called :data:`iris.config.TEST_DATA_DIR` @@ -202,7 +202,7 @@ be set by adding a ``test_data_dir`` entry to the ``Resources`` section of .. _grib_params: -Extended GRIB parameter translation +Extended GRIB Parameter Translation ----------------------------------- - More GRIB2 params are recognised on input. @@ -213,7 +213,7 @@ Extended GRIB parameter translation .. _one-d-linear: -One dimensional linear interpolation fix +One dimensional Linear Interpolation Fix ---------------------------------------- :func:`~iris.analysis.interpolate.linear` can now extrapolate from a single @@ -232,7 +232,7 @@ to cause the loss of coordinate metadata when calculating the curl or the derivative of a cube has been fixed. -Incompatible changes +Incompatible Changes ==================== * As part of simplifying the mechanism for accessing test data, diff --git a/docs/iris/src/whatsnew/1.5.rst b/docs/iris/src/whatsnew/1.5.rst index ea7965fe155..72bdbac480f 100644 --- a/docs/iris/src/whatsnew/1.5.rst +++ b/docs/iris/src/whatsnew/1.5.rst @@ -125,7 +125,7 @@ Features systems and mapping 0 to 360 longitudes to the -180 to 180 range. -Bugs fixed +Bugs Fixed ========== * NetCDF error handling on save has been extended to capture file path and diff --git a/docs/iris/src/whatsnew/1.6.rst b/docs/iris/src/whatsnew/1.6.rst index 3855d714794..8b0205b86f7 100644 --- a/docs/iris/src/whatsnew/1.6.rst +++ b/docs/iris/src/whatsnew/1.6.rst @@ -146,7 +146,7 @@ Features .. _caching: -A new utility function to assist with caching +A New Utility Function to Assist With Caching --------------------------------------------- To assist with management of caching results to file, the new utility function :func:`iris.util.file_is_newer_than` may be used to easily determine whether @@ -173,7 +173,7 @@ consuming processing, or to reap the benefit of fast-loading a pickled cube. .. _rms: -The RMS aggregator supports weights +The RMS Aggregator Supports Weights ----------------------------------- The :data:`iris.analysis.RMS` aggregator has been extended to allow the use of @@ -189,7 +189,7 @@ For example, an RMS weighted cube collapse is performed as follows: .. _equalise: -Equalise cube attributes +Equalise Cube Attributes ------------------------ To assist with :class:`iris.cube.Cube` merging, the new experimental in-place @@ -202,7 +202,7 @@ have the same attributes. .. _tolerance: -Masking a collapsed result by missing-data tolerance +Masking a Collapsed Result by Missing-Data Tolerance ---------------------------------------------------- The result from collapsing masked cube data may now be completely @@ -216,7 +216,7 @@ less than or equal to the provided tolerance. .. _promote: -Promote a scalar coordinate +Promote a Scalar Coordinate --------------------------- The new utility function :func:`iris.util.new_axis` creates a new cube with @@ -229,7 +229,7 @@ Note that, this function will load the data payload of the cube. .. _peak: -A new PEAK aggregator providing spline interpolation +A New PEAK Aggregator Providing Spline Interpolation ---------------------------------------------------- The new :data:`iris.analysis.PEAK` aggregator calculates the global peak @@ -244,7 +244,7 @@ For example, to calculate the peak time: collapsed_cube = cube.collapsed('time', PEAK) -Bugs fixed +Bugs Fixed ========== * :meth:`iris.cube.Cube.rolling_window` has been extended to support masked @@ -283,7 +283,7 @@ Bugs fixed * Exception no longer raised for any ellipsoid definition in nimrod loading. -Incompatible changes +Incompatible Changes ==================== * The experimental 'concatenate' function is now a method of a @@ -312,7 +312,7 @@ Incompatible changes been removed. ====================================== =========================================== - Removed property/method New method + Removed Property/Method New Method ====================================== =========================================== :meth:`~iris.unit.Unit.convertible()` :meth:`~iris.unit.Unit.is_convertible()` :attr:`~iris.unit.Unit.dimensionless` :meth:`~iris.unit.Unit.is_dimensionless()` @@ -335,7 +335,7 @@ Incompatible changes removed. =============================================================== ======================================================= - Removed function New function + Removed Function New Function =============================================================== ======================================================= :func:`~iris.coord_categorisation.add_custom_season` :func:`~iris.coord_categorisation.add_season` :func:`~iris.coord_categorisation.add_custom_season_number` :func:`~iris.coord_categorisation.add_season_number` diff --git a/docs/iris/src/whatsnew/1.7.rst b/docs/iris/src/whatsnew/1.7.rst index f6e818fedf6..44ebe9ec601 100644 --- a/docs/iris/src/whatsnew/1.7.rst +++ b/docs/iris/src/whatsnew/1.7.rst @@ -1,5 +1,5 @@ v1.7 (04 Jul 2014) -******************** +****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -196,7 +196,7 @@ Features * A speed improvement when loading PP or FF data and constraining on STASH code. -Bugs fixed +Bugs Fixed ========== * Data containing more than one reference cube for constructing hybrid height @@ -282,7 +282,7 @@ v1.7.4 (15 Apr 2015) create LambertConformal coordinate systems with Cartopy >= 0.12. -Incompatible changes +Incompatible Changes ==================== * Saving a cube with a STASH attribute to NetCDF now produces a variable diff --git a/docs/iris/src/whatsnew/1.8.rst b/docs/iris/src/whatsnew/1.8.rst index 579d4d20c58..0e327b4f5aa 100644 --- a/docs/iris/src/whatsnew/1.8.rst +++ b/docs/iris/src/whatsnew/1.8.rst @@ -1,5 +1,5 @@ v1.8 (14 Apr 2015) -******************** +****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -151,7 +151,7 @@ Features "iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid". -Bugs fixed +Bugs Fixed ========== * Fix in netCDF loader to correctly determine whether the longitude coordinate diff --git a/docs/iris/src/whatsnew/1.9.rst b/docs/iris/src/whatsnew/1.9.rst index c9d91bf33cb..9829d8ff3b2 100644 --- a/docs/iris/src/whatsnew/1.9.rst +++ b/docs/iris/src/whatsnew/1.9.rst @@ -1,5 +1,5 @@ v1.9 (10 Dec 2015) -******************** +****************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -93,7 +93,7 @@ Features read Fieldsfile data after the original :class:`iris.experimental.um.FieldsFileVariant` has been closed. -Bugs fixed +Bugs Fixed ========== * Fixed a bug in :meth:`iris.unit.Unit.convert` @@ -170,7 +170,7 @@ v1.9.2 (28 Jan 2016) * Fixed a bug regarding unsuccessful dot import. -Incompatible changes +Incompatible Changes ==================== * GRIB message/file reading and writing may not be available for Python 3 due diff --git a/docs/iris/src/whatsnew/2.0.rst b/docs/iris/src/whatsnew/2.0.rst index fbd012dd1f3..400a395e907 100644 --- a/docs/iris/src/whatsnew/2.0.rst +++ b/docs/iris/src/whatsnew/2.0.rst @@ -60,7 +60,7 @@ Features respectively. -The :data:`iris.FUTURE` has arrived! +The :data:`iris.FUTURE` has Arrived! ------------------------------------ Throughout version 1 of Iris a set of toggles in @@ -111,7 +111,7 @@ all existing toggles in :attr:`iris.FUTURE` now default to :data:`True`. off is now deprecated. -Bugs fixed +Bugs Fixed ========== * Indexing or slicing an :class:`~iris.coords.AuxCoord` coordinate will return a coordinate with diff --git a/docs/iris/src/whatsnew/2.1.rst b/docs/iris/src/whatsnew/2.1.rst index ef03f023b2c..18c562d3da0 100644 --- a/docs/iris/src/whatsnew/2.1.rst +++ b/docs/iris/src/whatsnew/2.1.rst @@ -43,7 +43,7 @@ Features the ``standard_parallel`` keyword argument (:pull:`3041`). -Bugs fixed +Bugs Fixed ========== * All var names being written to NetCDF are now CF compliant. @@ -59,7 +59,7 @@ Bugs fixed ``axes`` keyword (:pull:`3010`). -Incompatible changes +Incompatible Changes ==================== * The deprecated :mod:`iris.experimental.um` was removed. @@ -94,4 +94,4 @@ Internal * Iris now requires version 2 of Matplotlib, and ``>=1.14`` of NumPy. Full requirements can be seen in the `requirements `_ - directory of the Iris' the source. \ No newline at end of file + directory of the Iris' the source. diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/iris/src/whatsnew/2.2.rst index 48280895fed..a1f48f962b2 100644 --- a/docs/iris/src/whatsnew/2.2.rst +++ b/docs/iris/src/whatsnew/2.2.rst @@ -66,7 +66,7 @@ Features a NaN-tolerant array comparison. -Bugs fixed +Bugs Fixed ========== * The bug has been fixed that prevented printing time coordinates with bounds diff --git a/docs/iris/src/whatsnew/2.3.rst b/docs/iris/src/whatsnew/2.3.rst index 5997a7f4dc1..2509242c057 100644 --- a/docs/iris/src/whatsnew/2.3.rst +++ b/docs/iris/src/whatsnew/2.3.rst @@ -147,7 +147,7 @@ Features `metarelate/metOcean commit 448f2ef, 2019-11-29 `_ -Bugs fixed +Bugs Fixed ========== * Cube equality of boolean data is now handled correctly. diff --git a/docs/iris/src/whatsnew/2.4.rst b/docs/iris/src/whatsnew/2.4.rst index c62e84c1296..0e271389b5b 100644 --- a/docs/iris/src/whatsnew/2.4.rst +++ b/docs/iris/src/whatsnew/2.4.rst @@ -47,7 +47,7 @@ Features ``STASH`` from the attributes dictionary of a :class:`~iris.cube.Cube`. -Bugs fixed +Bugs Fixed ========== * Fixed a problem which was causing file loads to fetch *all* field data diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 0a9dcd89b0a..399325add52 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -1,6 +1,6 @@ .. include:: ../common_links.inc -v3.0 (02 Oct 2020) +v3.0 (25 Jan 2021) ****************** This document explains the changes made to Iris for this release @@ -113,6 +113,12 @@ This document explains the changes made to Iris for this release and preservation of common metadata and coordinates during cube math operations. Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) +* `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when + collapsing over a single dimension. + Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. + The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works + with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) + 🐛 Bugs Fixed ============= @@ -168,7 +174,11 @@ This document explains the changes made to Iris for this release Previously, the first tick label would occasionally be duplicated. This also removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) -* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) +* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check + ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) + +* `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's + coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) .. _whatsnew 3.0 changes: @@ -356,6 +366,11 @@ This document explains the changes made to Iris for this release * `@jonseddon`_ updated the CF version of the netCDF saver in the :ref:`saving_iris_cubes` section and in the equivalent function docstring. + (:pull:`3925`) + +* `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. + (:pull:`3940`) + 💼 Internal =========== @@ -427,7 +442,7 @@ This document explains the changes made to Iris for this release * `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). -* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_. (:pull:`3928`) +* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ support. (:pull:`3928`) * `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to @@ -463,6 +478,7 @@ This document explains the changes made to Iris for this release .. _@tkknight: https://github.com/tkknight .. _@lbdreyer: https://github.com/lbdreyer .. _@SimonPeatman: https://github.com/SimonPeatman +.. _@TomekTrzeciak: https://github.com/TomekTrzeciak .. _@rcomer: https://github.com/rcomer .. _@jvegasbsc: https://github.com/jvegasbsc .. _@zklaus: https://github.com/zklaus @@ -481,3 +497,6 @@ This document explains the changes made to Iris for this release .. _CF Conventions and Metadata: https://cfconventions.org/ .. _flake8: https://flake8.pycqa.org/en/stable/ .. _nox: https://nox.thea.codes/en/stable/ +.. _Title Case Capitalization: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case +.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _stickler-ci: https://stickler-ci.com/ diff --git a/docs/iris/src/whatsnew/index.rst b/docs/iris/src/whatsnew/index.rst index 3fd5fe60700..c7fabc0479a 100644 --- a/docs/iris/src/whatsnew/index.rst +++ b/docs/iris/src/whatsnew/index.rst @@ -1,6 +1,6 @@ .. _iris_whatsnew: -What's new in Iris +What's New in Iris ****************** These "What's new" pages describe the important changes between major diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index e31c7b58d7a..a78d0a76820 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -106,7 +106,7 @@ def callback(cube, field, filename): # Iris revision. -__version__ = "3.1.dev0" +__version__ = "3.1.0dev0" # Restrict the names imported when using "from iris import *" __all__ = [ diff --git a/lib/iris/common/resolve.py b/lib/iris/common/resolve.py index ad372478097..e772eeefce0 100644 --- a/lib/iris/common/resolve.py +++ b/lib/iris/common/resolve.py @@ -230,7 +230,7 @@ def __init__(self, lhs=None, rhs=None): """ #: The ``lhs`` operand to be resolved into the resultant :class:`~iris.cube.Cube`. - self.lhs_cube = None # set in _call__ + self.lhs_cube = None # set in __call__ #: The ``rhs`` operand to be resolved into the resultant :class:`~iris.cube.Cube`. self.rhs_cube = None # set in __call__ @@ -294,6 +294,25 @@ def __init__(self, lhs=None, rhs=None): self(lhs, rhs) def __call__(self, lhs, rhs): + """ + Resolve the ``lhs`` :class:`~iris.cube.Cube` operand and ``rhs`` + :class:`~iris.cube.Cube` operand metadata. + + Involves determining all the common coordinate metadata shared between + the operands, and the metadata that is local to each operand. Given + the common metadata, the broadcast shape of the resultant resolved + :class:`~iris.cube.Cube`, which may be auto-transposed, can be + determined. + + Args: + + * lhs: + The left-hand-side :class:`~iris.cube.Cube` operand. + + * rhs: + The right-hand-side :class:`~iris.cube.Cube` operand. + + """ from iris.cube import Cube emsg = ( @@ -338,11 +357,31 @@ def __call__(self, lhs, rhs): return self def _as_compatible_cubes(self): + """ + Determine whether the ``src`` and ``tgt`` :class:`~iris.cube.Cube` can + be transposed and/or broadcast successfully together. + + If compatible, the ``_broadcast_shape`` of the resultant resolved cube is + calculated, and the ``_src_cube_resolved`` (transposed/broadcast ``src`` + cube) and ``_tgt_cube_resolved`` (same as the ``tgt`` cube) are + calculated. + + An exception will be raised if the ``src`` and ``tgt`` cannot be + broadcast, even after a suitable transpose has been performed. + + .. note:: + + Requires that **all** ``src`` cube dimensions have been mapped + successfully to an appropriate ``tgt`` cube dimension. + + """ from iris.cube import Cube src_cube = self._src_cube tgt_cube = self._tgt_cube + assert src_cube.ndim == len(self.mapping) + # Use the mapping to calculate the new src cube shape. new_src_shape = [1] * tgt_cube.ndim for src_dim, tgt_dim in self.mapping.items(): @@ -430,6 +469,40 @@ def _aux_coverage( common_aux_metadata, common_scalar_metadata, ): + """ + Determine the dimensions covered by each of the local and common + auxiliary coordinates of the provided :class:`~iris.cube.Cube`. + + The cube dimensions not covered by any of the auxiliary coordinates is + also determined; these are known as `free` dimensions. + + The scalar coordinates local to the cube are also determined. + + Args: + + * cube: + The :class:`~iris.cube.Cube` to be analysed for coverage. + + * cube_items_aux: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each auxiliary coordinate owned by the cube. + + * cube_items_scalar: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each scalar coordinate owned by the cube. + + * common_aux_metadata: + The list of common auxiliary coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + * common_scalar_metadata: + The list of common scalar coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + Returns: + :class:`~iris.common.resolve._AuxCoverage` + + """ common_items_aux = [] common_items_scalar = [] local_items_aux = [] @@ -465,7 +538,33 @@ def _aux_coverage( dims_free=sorted(dims_free), ) - def _aux_mapping(self, src_coverage, tgt_coverage): + @staticmethod + def _aux_mapping(src_coverage, tgt_coverage): + """ + Establish the mapping of dimensions from the ``src`` to ``tgt`` + :class:`~iris.cube.Cube` using the auxiliary coordinate metadata + common between each of the operands. + + The ``src`` to ``tgt`` common auxiliary coordinate mapping is held by + the :attr:`~iris.common.resolve.Resolve.mapping`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube` i.e., map from the common ``src`` + dimensions. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube` i.e., map to the common ``tgt`` + dimensions. + + Returns: + Dictionary of ``src`` to ``tgt`` dimension mapping. + + """ + mapping = {} for tgt_item in tgt_coverage.common_items_aux: # Search for a src aux metadata match. tgt_metadata = tgt_item.metadata @@ -484,7 +583,7 @@ def _aux_mapping(self, src_coverage, tgt_coverage): tgt_dims = tgt_item.dims if len(src_dims) == len(tgt_dims): for src_dim, tgt_dim in zip(src_dims, tgt_dims): - self.mapping[src_dim] = tgt_dim + mapping[src_dim] = tgt_dim logger.debug(f"{src_dim}->{tgt_dim}") else: # This situation can only occur due to a systemic internal @@ -504,9 +603,26 @@ def _aux_mapping(self, src_coverage, tgt_coverage): tgt_item.dims, ) ) + return mapping @staticmethod def _categorise_items(cube): + """ + Inspect the provided :class:`~iris.cube.Cube` and group its + coordinates and associated metadata into dimension, auxiliary and + scalar categories. + + Args: + + * cube: + The :class:`~iris.cube.Cube` that will have its coordinates and + metadata grouped into their associated dimension, auxiliary and + scalar categories. + + Returns: + :class:`~iris.common.resolve._CategoryItems` + + """ category = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) # Categorise the dim coordinates of the cube. @@ -530,15 +646,40 @@ def _categorise_items(cube): return category @staticmethod - def _create_prepared_item(coord, dims, src=None, tgt=None): - if src is not None and tgt is not None: - combined = src.combine(tgt) + def _create_prepared_item( + coord, dims, src_metadata=None, tgt_metadata=None + ): + """ + Convenience method that creates a :class:`~iris.common.resolve._PreparedItem` + containing the data and metadata required to construct and attach a coordinate + to the resultant resolved cube. + + Args: + + * coord: + The coordinate with the ``points`` and ``bounds`` to be extracted. + + * dims: + The dimensions that the ``coord`` spans on the resulting resolved :class:`~iris.cube.Cube`. + + * src_metadata: + The coordinate metadata from the ``src`` :class:`~iris.cube.Cube`. + + * tgt_metadata: + The coordinate metadata from the ``tgt`` :class:`~iris.cube.Cube`. + + Returns: + The :class:`~iris.common.resolve._PreparedItem`. + + """ + if src_metadata is not None and tgt_metadata is not None: + combined = src_metadata.combine(tgt_metadata) else: - combined = src or tgt + combined = src_metadata or tgt_metadata if not isinstance(dims, Iterable): dims = (dims,) prepared_metadata = _PreparedMetadata( - combined=combined, src=src, tgt=tgt + combined=combined, src=src_metadata, tgt=tgt_metadata ) bounds = coord.bounds result = _PreparedItem( @@ -573,6 +714,30 @@ def _show(items, heading): @staticmethod def _dim_coverage(cube, cube_items_dim, common_dim_metadata): + """ + Determine the dimensions covered by each of the local and common + dimension coordinates of the provided :class:`~iris.cube.Cube`. + + The cube dimensions not covered by any of the dimension coordinates is + also determined; these are known as `free` dimensions. + + Args: + + * cube: + The :class:`~iris.cube.Cube` to be analysed for coverage. + + * cube_items_dim: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each dimension coordinate owned by the cube. + + * common_dim_metadata: + The list of common dimension coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + Returns: + :class:`~iris.common.resolve._DimCoverage` + + """ ndim = cube.ndim metadata = [None] * ndim coords = [None] * ndim @@ -599,13 +764,39 @@ def _dim_coverage(cube, cube_items_dim, common_dim_metadata): dims_free=sorted(dims_free), ) - def _dim_mapping(self, src_coverage, tgt_coverage): + @staticmethod + def _dim_mapping(src_coverage, tgt_coverage): + """ + Establish the mapping of dimensions from the ``src`` to ``tgt`` + :class:`~iris.cube.Cube` using the dimension coordinate metadata + common between each of the operands. + + The ``src`` to ``tgt`` common dimension coordinate mapping is held by + the :attr:`~iris.common.resolve.Resolve.mapping`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube` i.e., map from the common ``src`` + dimensions. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube` i.e., map to the common ``tgt`` + dimensions. + + Returns: + Dictionary of ``src`` to ``tgt`` dimension mapping. + + """ + mapping = {} for tgt_dim in tgt_coverage.dims_common: # Search for a src dim metadata match. tgt_metadata = tgt_coverage.metadata[tgt_dim] try: src_dim = src_coverage.metadata.index(tgt_metadata) - self.mapping[src_dim] = tgt_dim + mapping[src_dim] = tgt_dim logger.debug(f"{src_dim}->{tgt_dim}") except ValueError: # This exception can only occur due to a systemic internal @@ -621,9 +812,10 @@ def _dim_mapping(self, src_coverage, tgt_coverage): src_coverage.cube.name(), tgt_coverage.cube.name(), tgt_metadata, - tuple([tgt_dim]), + (tgt_dim,), ) ) + return mapping def _free_mapping( self, @@ -632,6 +824,57 @@ def _free_mapping( src_aux_coverage, tgt_aux_coverage, ): + """ + Attempt to update the :attr:`~iris.common.resolve.Resolve.mapping` with + ``src`` to ``tgt`` :class:`~iris.cube.Cube` mappings from unmapped ``src`` + dimensions that are free from coordinate metadata coverage to ``tgt`` + dimensions that have local metadata coverage (i.e., is not common between + the ``src`` and ``tgt``) or dimensions that are free from coordinate + metadata coverage. + + If the ``src`` :class:`~iris.cube.Cube` does not have any free dimensions, + the attempt to map unmapped ``tgt`` dimensions that have local metadata + coverage to ``src`` dimensions that are free from coordinate metadata + coverage. + + An exception will be raised if there are any ``src`` :class:`~iris.cube.Cube` + dimensions not mapped to an associated ``tgt`` dimension. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube`. + + * src_aux_coverage: + The :class:`~iris.common.resolve._AuxCoverage` of the ``src`` + :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:`~iris.common.resolve._AuxCoverage` of the ``tgt`` + :class:`~iris.cube.Cube`. + + .. note:: + + All unmapped dimensions with an extend >1 are mapped before those + with an extent of 1, as such dimensions cannot be broadcast. It + is important to map specific non-broadcastable dimensions before + generic broadcastable dimensions otherwise we are open to failing to + map all the src dimensions as a generic src broadcast dimension has + been mapped to the only tgt dimension that a specific non-broadcastable + dimension can be mapped to. + + .. note:: + + A local dimension cannot be mapped to another local dimension, + by definition, otherwise this dimension would be classed as a + common dimension. + + """ src_cube = src_dim_coverage.cube tgt_cube = tgt_dim_coverage.cube src_ndim = src_cube.ndim @@ -663,11 +906,16 @@ def _free_mapping( tgt_shape = tgt_cube.shape src_max, tgt_max = max(src_shape), max(tgt_shape) - def assign_mapping(extent, unmapped_local_items, free_items=None): + def _assign_mapping(extent, unmapped_local_items, free_items=None): result = None if free_items is None: free_items = [] if extent == 1: + # Map to the first available unmapped local dimension or + # the first available free dimension. + # Dimension shape doesn't matter here as the extent is 1, + # therefore broadcasting will take care of any discrepency + # between src and tgt dimension extent. if unmapped_local_items: result, _ = unmapped_local_items.pop(0) elif free_items: @@ -680,10 +928,10 @@ def _filter(items): ) def _pop(item, items): - result, _ = item + dim, _ = item index = items.index(item) items.pop(index) - return result + return dim items = _filter(unmapped_local_items) if items: @@ -700,11 +948,12 @@ def _pop(item, items): (dim, tgt_shape[dim]) for dim in tgt_unmapped_local ] tgt_free_items = [(dim, tgt_shape[dim]) for dim in tgt_free] + # Sort by decreasing src dimension extent and increasing src dimension + # as we want broadcast src dimensions to be mapped last. + src_key_func = lambda dim: (src_max - src_shape[dim], dim) - for src_dim in sorted( - src_free, key=lambda dim: (src_max - src_shape[dim], dim) - ): - tgt_dim = assign_mapping( + for src_dim in sorted(src_free, key=src_key_func): + tgt_dim = _assign_mapping( src_shape[src_dim], tgt_unmapped_local_items, tgt_free_items, @@ -725,11 +974,12 @@ def _pop(item, items): src_unmapped_local_items = [ (dim, src_shape[dim]) for dim in src_unmapped_local ] + # Sort by decreasing tgt dimension extent and increasing tgt dimension + # as we want broadcast tgt dimensions to be mapped last. + tgt_key_func = lambda dim: (tgt_max - tgt_shape[dim], dim) - for tgt_dim in sorted( - tgt_free, key=lambda dim: (tgt_max - tgt_shape[dim], dim) - ): - src_dim = assign_mapping( + for tgt_dim in sorted(tgt_free, key=tgt_key_func): + src_dim = _assign_mapping( tgt_shape[tgt_dim], src_unmapped_local_items ) if src_dim is not None: @@ -758,6 +1008,17 @@ def _pop(item, items): logger.debug(f"mapping free dimensions gives, mapping={self.mapping}") def _metadata_coverage(self): + """ + Using the pre-categorised metadata of the cubes, determine the dimensions + covered by their associated dimension and auxiliary coordinates, and which + dimensions are free of metadata coverage. + + This coverage analysis clarifies how the dimensions covered by common + metadata are related, thus establishing a dimensional mapping between + the cubes. It also identifies the dimensions covered by metadata that + is local to each cube, and indeed which dimensions are free of metadata. + + """ # Determine the common dim coordinate metadata coverage. common_dim_metadata = [ item.metadata for item in self.category_common.items_dim @@ -798,6 +1059,37 @@ def _metadata_coverage(self): ) def _metadata_mapping(self): + """ + Ensure that each ``src`` :class:`~iris.cube.Cube` dimension is mapped to an associated + ``tgt`` :class:`~iris.cube.Cube` dimension using the common dim and aux coordinate metadata. + + If the common metadata does not result in a full mapping of ``src`` to ``tgt`` dimensions + then free dimensions are analysed to determine whether the mapping can be completed. + + Once the ``src`` has been mapped to the ``tgt``, the cubes are checked to ensure that they + will successfully broadcast, and the ``src`` :class:`~iris.cube.Cube` is transposed appropriately, + if necessary. + + The :attr:`~iris.common.resolve.Resolve._broadcast_shape` is set, along with the + :attr:`~iris.common.resolve.Resolve._src_cube_resolved` and :attr:`~iris.common.resolve.Resolve._tgt_cube_resolved`, + which are the broadcast/transposed ``src`` and ``tgt``. + + .. note:: + + An exception will be raised if a ``src`` dimension cannot be mapped to a ``tgt`` dimension. + + .. note:: + + An exception will be raised if the full mapped ``src`` :class:`~iris.cube.Cube` cannot be + broadcast or transposed with the ``tgt`` :class:`~iris.cube.Cube`. + + .. note:: + + The ``src`` and ``tgt`` may be swapped in the case where they both have equal dimensionality and + the ``tgt`` does have the same shape as the resolved broadcast shape (and the ``src`` does) or + the ``tgt`` has more free dimensions than the ``src``. + + """ # Initialise the state. self.mapping = {} @@ -819,7 +1111,9 @@ def _metadata_mapping(self): # Use the dim coordinates to fully map the # src cube dimensions to the tgt cube dimensions. - self._dim_mapping(src_dim_coverage, tgt_dim_coverage) + self.mapping.update( + self._dim_mapping(src_dim_coverage, tgt_dim_coverage) + ) logger.debug( f"mapping common dim coordinates gives, mapping={self.mapping}" ) @@ -827,7 +1121,9 @@ def _metadata_mapping(self): # If necessary, use the aux coordinates to fully map the # src cube dimensions to the tgt cube dimensions. if not self.mapped: - self._aux_mapping(src_aux_coverage, tgt_aux_coverage) + self.mapping.update( + self._aux_mapping(src_aux_coverage, tgt_aux_coverage) + ) logger.debug( f"mapping common aux coordinates, mapping={self.mapping}" ) @@ -886,6 +1182,12 @@ def _metadata_mapping(self): self._as_compatible_cubes() def _metadata_prepare(self): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_category` and + :attr:`~iris.common.resolve.Resolve.prepared_factories` with the necessary metadata to be constructed + and attached to the resulting resolved :class:`~iris.cube.Cube`. + + """ # Initialise the state. self.prepared_category = _CategoryItems( items_dim=[], items_aux=[], items_scalar=[] @@ -1053,6 +1355,41 @@ def _prepare_common_aux_payload( prepared_items, ignore_mismatch=None, ): + """ + Populate the ``prepared_items`` with a :class:`~iris.common.resolve._PreparedItem` containing + the necessary metadata for each auxiliary coordinate to be constructed and attached to the + resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + For mixed ``src`` and ``tgt`` coordinate types with matching metadata, an + :class:`~iris.coords.AuxCoord` will be nominated for construction. + + Args: + + * src_common_items: + The list of :attr:`~iris.common.resolve._AuxCoverage.common_items_aux` metadata + for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_common_items: + The list of :attr:`~iris.common.resolve._AuxCoverage.common_items_aux` metadata + for the ``tgt`` :class:`~iris.cube.Cube`. + + * prepared_items: + The list of :class:`~iris.common.resolve._PreparedItem` metadata that will be used + to construct the auxiliary coordinates that will be attached to the resulting + resolved :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + When ``False``, an exception will be raised if a difference is detected between corresponding + ``src`` and ``tgt`` coordinate ``points`` and/or ``bounds``. + When ``True``, the coverage metadata is ignored i.e., a coordinate will not be constructed and + added to the resulting resolved :class:`~iris.cube.Cube`. + Defaults to ``False``. + + """ from iris.coords import AuxCoord if ignore_mismatch is None: @@ -1115,6 +1452,30 @@ def _prepare_common_aux_payload( def _prepare_common_dim_payload( self, src_coverage, tgt_coverage, ignore_mismatch=None ): + """ + Populate the ``items_dim`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for + each :class:`~iris.coords.DimCoord` to be constructed and attached to the resulting resolved + :class:`~iris.cube.Cube`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` metadata for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` metadata for the ``tgt`` :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + When ``False``, an exception will be raised if a difference is detected between corresponding + ``src`` and ``tgt`` :class:`~iris.coords.DimCoord` ``points`` and/or ``bounds``. + When ``True``, the coverage metadata is ignored i.e., a :class:`~iris.coords.DimCoord` will not + be constructed and added to the resulting resolved :class:`~iris.cube.Cube`. + Defaults to ``False``. + + """ from iris.coords import DimCoord if ignore_mismatch is None: @@ -1153,55 +1514,123 @@ def _prepare_common_dim_payload( ) self.prepared_category.items_dim.append(prepared_item) - def _prepare_factory_payload(self, cube, category_local, from_src=True): - def _get_prepared_item(metadata, from_src=True, from_local=False): - result = None - if from_local: - category = category_local - match = lambda item: item.metadata == metadata + def _get_prepared_item( + self, metadata, category_local, from_src=True, from_local=False + ): + """ + Find the :attr:`~iris.common.resolve._PreparedItem` from the + :attr:`~iris.common.resolve.Resolve.prepared_category` that matches the provided ``metadata``. + + Alternatively, the ``category_local`` is searched to find a :class:`~iris.common.resolve._Item` + with matching ``metadata`` from either the local ``src`` or ``tgt`` :class:`~iris.cube.Cube`. + If a match is found, then a new `~iris.common.resolve._PreparedItem` is created and added to + :attr:`~iris.common.resolve.Resolve.prepared_category` and returned. See ``from_local``. + + Args: + + * metadata: + The target metadata of the prepared (or local) item to retrieve. + + * category_local: + The :class:`~iris.common.resolve._CategoryItems` containing the + local metadata of either the ``src`` or ``tgt`` :class:`~iris.cube.Cube`. + See ``from_local``. + + Kwargs: + + * from_src: + Boolean stating whether the ``metadata`` is from the ``src`` (``True``) + or ``tgt`` :class:`~iris.cube.Cube`. + Defaults to ``True``. + + * from_local: + Boolean controlling whether the ``metadata`` is used to search the + ``category_local`` (``True``) or the :attr:`~iris.common.resolve.Resolve.prepared_category`. + Defaults to ``False``. + + Returns: + The :class:`~iris.common.resolve._PreparedItem` matching the provided ``metadata``. + + """ + result = None + + if from_local: + category = category_local + match = lambda item: item.metadata == metadata + else: + category = self.prepared_category + if from_src: + match = lambda item: item.metadata.src == metadata else: - category = self.prepared_category - if from_src: - match = lambda item: item.metadata.src == metadata + match = lambda item: item.metadata.tgt == metadata + + for member in category._fields: + category_items = getattr(category, member) + matched_items = tuple(filter(match, category_items)) + if matched_items: + if len(matched_items) > 1: + dmsg = ( + f"ignoring factory dependency {metadata}, multiple {'src' if from_src else 'tgt'} " + f"{'local' if from_local else 'prepared'} metadata matches" + ) + logger.debug(dmsg) else: - match = lambda item: item.metadata.tgt == metadata - for member in category._fields: - category_items = getattr(category, member) - matched_items = tuple(filter(match, category_items)) - if matched_items: - if len(matched_items) > 1: - dmsg = ( - f"ignoring factory dependency {metadata}, multiple {'src' if from_src else 'tgt'} " - f"{'local' if from_local else 'prepared'} metadata matches" - ) - logger.debug(dmsg) - else: - (item,) = matched_items - if from_local: - src = tgt = None - if from_src: - src = item.metadata - dims = tuple( - [self.mapping[dim] for dim in item.dims] - ) - else: - tgt = item.metadata - dims = item.dims - result = self._create_prepared_item( - item.coord, dims, src=src, tgt=tgt - ) - getattr(self.prepared_category, member).append( - result + (item,) = matched_items + if from_local: + src = tgt = None + if from_src: + src = item.metadata + dims = tuple( + [self.mapping[dim] for dim in item.dims] ) else: - result = item - break - return result + tgt = item.metadata + dims = item.dims + result = self._create_prepared_item( + item.coord, + dims, + src_metadata=src, + tgt_metadata=tgt, + ) + getattr(self.prepared_category, member).append(result) + else: + result = item + break + return result + + def _prepare_factory_payload(self, cube, category_local, from_src=True): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_factories` with a :class:`~iris.common.resolve._PreparedFactory` + containing the necessary metadata for each ``src`` and/or ``tgt`` auxiliary factory to be constructed and + attached to the resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + The required dependencies of an auxiliary factory may not all be available in the + :attr:`~iris.common.resolve.Resolve.prepared_category` and therefore this is a legitimate + reason to add the associated metadata of the local dependency to the ``prepared_category``. + + Args: + + * cube: + The :class:`~iris.cube.Cube` that may contain an auxiliary factory to be prepared. + + * category_local: + The :class:`~iris.common.resolve._CategoryItems` of all metadata local to the provided ``cube``. + Kwargs: + + * from_src: + Boolean stating whether the provided ``cube`` is either a ``src`` or ``tgt`` + :class:`~iris.cube.Cube` - used to retrieve the appropriate metadata from a + :class:`~iris.common.resolve._PreparedMetadata`. + + """ for factory in cube.aux_factories: container = type(factory) dependencies = {} prepared_item = None + found = True if tuple( filter( @@ -1222,18 +1651,24 @@ def _get_prepared_item(metadata, from_src=True, from_local=False): dependency_coord, ) in factory.dependencies.items(): metadata = dependency_coord.metadata - prepared_item = _get_prepared_item(metadata, from_src=from_src) + prepared_item = self._get_prepared_item( + metadata, category_local, from_src=from_src + ) if prepared_item is None: - prepared_item = _get_prepared_item( - metadata, from_src=from_src, from_local=True + prepared_item = self._get_prepared_item( + metadata, + category_local, + from_src=from_src, + from_local=True, ) if prepared_item is None: dmsg = f"cannot find matching {metadata} for {container} dependency {dependency_name}" logger.debug(dmsg) + found = False break dependencies[dependency_name] = prepared_item.metadata - if prepared_item is not None: + if found and prepared_item is not None: prepared_factory = _PreparedFactory( container=container, dependencies=dependencies ) @@ -1243,6 +1678,29 @@ def _get_prepared_item(metadata, from_src=True, from_local=False): logger.debug(dmsg) def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): + """ + Populate the ``items_aux`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local auxiliary coordinate to be constructed and attached to the resulting + resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, lenient behaviour subscribes to the philosophy that it is easier to remove + metadata than it is to find then add metadata. To those ends, lenient behaviour supports + metadata richness by adding both local ``src`` and ``tgt`` auxiliary coordinates. + Alternatively, strict behaviour will only add a ``tgt`` local auxiliary coordinate that + spans dimensions not mapped to by the ``src`` e.g., extra ``tgt`` dimensions. + + Args: + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Determine whether there are tgt dimensions not mapped to by an # associated src dimension, and thus may be covered by any local # tgt aux coordinates. @@ -1259,7 +1717,7 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): if all([dim in mapped_src_dims for dim in item.dims]): tgt_dims = tuple([self.mapping[dim] for dim in item.dims]) prepared_item = self._create_prepared_item( - item.coord, tgt_dims, src=item.metadata + item.coord, tgt_dims, src_metadata=item.metadata ) self.prepared_category.items_aux.append(prepared_item) else: @@ -1281,7 +1739,7 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): [dim in extra_tgt_dims for dim in tgt_dims] ): prepared_item = self._create_prepared_item( - item.coord, tgt_dims, tgt=item.metadata + item.coord, tgt_dims, tgt_metadata=item.metadata ) self.prepared_category.items_aux.append(prepared_item) else: @@ -1293,6 +1751,28 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): logger.debug(dmsg) def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): + """ + Populate the ``items_dim`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local :class:`~iris.coords.DimCoord` to be constructed and attached to the + resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, a local coordinate will only be added if there is no other metadata competing + to describe the same dimension/s on the ``tgt`` :class:`~iris.cube.Cube`. Lenient behaviour + is more liberal, whereas strict behaviour will only add a local ``tgt`` coordinate covering + an unmapped "extra" ``tgt`` dimension/s. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ mapped_tgt_dims = self.mapping.values() # Determine whether there are tgt dimensions not mapped to by an @@ -1314,7 +1794,7 @@ def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): metadata = src_dim_coverage.metadata[src_dim] coord = src_dim_coverage.coords[src_dim] prepared_item = self._create_prepared_item( - coord, tgt_dim, src=metadata + coord, tgt_dim, src_metadata=metadata ) self.prepared_category.items_dim.append(prepared_item) else: @@ -1347,13 +1827,36 @@ def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): if metadata is not None: coord = tgt_dim_coverage.coords[tgt_dim] prepared_item = self._create_prepared_item( - coord, tgt_dim, tgt=metadata + coord, tgt_dim, tgt_metadata=metadata ) self.prepared_category.items_dim.append(prepared_item) def _prepare_local_payload_scalar( self, src_aux_coverage, tgt_aux_coverage ): + """ + Populate the ``items_scalar`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local scalar coordinate to be constructed and attached to the resulting + resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, lenient behaviour subscribes to the philosophy that it is easier to remove + metadata than it is to find then add metadata. To those ends, lenient behaviour supports + metadata richness by adding both local ``src`` and ``tgt`` scalar coordinates. + Alternatively, strict behaviour will only add a ``tgt`` local scalar coordinate when the + ``src`` is a scalar :class:`~iris.cube.Cube` with no local scalar coordinates. + + Args: + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Add all local tgt scalar coordinates iff the src cube is a # scalar cube with no local src scalar coordinates. # Only for strict maths. @@ -1367,14 +1870,14 @@ def _prepare_local_payload_scalar( # Add any local src scalar coordinates, if available. for item in src_aux_coverage.local_items_scalar: prepared_item = self._create_prepared_item( - item.coord, item.dims, src=item.metadata + item.coord, item.dims, src_metadata=item.metadata ) self.prepared_category.items_scalar.append(prepared_item) # Add any local tgt scalar coordinates, if available. for item in tgt_aux_coverage.local_items_scalar: prepared_item = self._create_prepared_item( - item.coord, item.dims, tgt=item.metadata + item.coord, item.dims, tgt_metadata=item.metadata ) self.prepared_category.items_scalar.append(prepared_item) @@ -1385,6 +1888,27 @@ def _prepare_local_payload( tgt_dim_coverage, tgt_aux_coverage, ): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_category_items` with a + :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata from the ``src`` + and/or ``tgt`` :class:`~iris.cube.Cube` for each coordinate to be constructed and attached + to the resulting resolved :class:`~iris.cube.Cube`. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Add local src/tgt dim coordinates. self._prepare_local_payload_dim(src_dim_coverage, tgt_dim_coverage) @@ -1397,6 +1921,47 @@ def _prepare_local_payload( def _prepare_points_and_bounds( self, src_coord, tgt_coord, src_dims, tgt_dims, ignore_mismatch=None ): + """ + Compare the points and bounds of the ``src`` and ``tgt`` coordinates to ensure + that they are equivalent, taking into account broadcasting when appropriate. + + .. note:: + + An exception will be raised if the ``src`` and ``tgt`` coordinates cannot + be broadcast. + + .. note:: + + An exception will be raised if either the points or bounds are different, + however appropriate lenient behaviour concessions are applied. + + Args: + + * src_coord: + The ``src`` :class:`~iris.cube.Cube` coordinate with metadata matching + the ``tgt_coord``. + + * tgt_coord: + The ``tgt`` :class`~iris.cube.Cube` coordinate with metadata matching + the ``src_coord``. + + * src_dims: + The dimension/s of the ``src_coord`` attached to the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dims: + The dimension/s of the ``tgt_coord`` attached to the ``tgt`` :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + For lenient behaviour only, don't raise an exception if there is a difference between + the ``src`` and ``tgt`` coordinate points or bounds. + Defaults to ``False``. + + Returns: + Tuple of equivalent ``points`` and ``bounds``, otherwise ``None``. + + """ from iris.util import array_equal if ignore_mismatch is None: @@ -1443,6 +2008,7 @@ def _prepare_points_and_bounds( tgt_broadcasting = tgt_shape != tgt_shape_broadcast if src_broadcasting and tgt_broadcasting: + # TBD: Extend capability to support attempting to broadcast two-way multi-dimensional coordinates. emsg = ( f"Cannot broadcast the coordinate {src_coord.name()!r} on " f"{self._src_cube_position} cube {self._src_cube.name()!r} and " diff --git a/lib/iris/cube.py b/lib/iris/cube.py index bb631cae734..7c7d6c58e9a 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -981,9 +981,7 @@ def convert_units(self, unit): celsius and subtract 273.15 from each value in :attr:`~iris.cube.Cube.data`. - .. warning:: - Calling this method will trigger any deferred loading, causing - the cube's data array to be loaded into memory. + This operation preserves lazy data. """ # If the cube has units convert the data. @@ -2186,23 +2184,20 @@ def _summary_coord_extra(self, coord, indent): extra = "" similar_coords = self.coords(coord.name()) if len(similar_coords) > 1: - # Find all the attribute keys - keys = set() - for similar_coord in similar_coords: - keys.update(similar_coord.attributes.keys()) - # Look for any attributes that vary + similar_coords.remove(coord) + # Look for any attributes that vary. vary = set() - attributes = {} - for key in keys: + for key, value in coord.attributes.items(): for similar_coord in similar_coords: if key not in similar_coord.attributes: vary.add(key) break - value = similar_coord.attributes[key] - if attributes.setdefault(key, value) != value: + if not np.array_equal( + similar_coord.attributes[key], value + ): vary.add(key) break - keys = sorted(vary & set(coord.attributes.keys())) + keys = sorted(vary) bits = [ "{}={!r}".format(key, coord.attributes[key]) for key in keys ] @@ -3923,10 +3918,15 @@ def collapsed(self, coords, aggregator, **kwargs): # on the cube lazy array. # NOTE: do not reform the data in this case, as 'lazy_aggregate' # accepts multiple axes (unlike 'aggregate'). - collapse_axis = list(dims_to_collapse) + collapse_axes = list(dims_to_collapse) + if len(collapse_axes) == 1: + # Replace a "list of 1 axes" with just a number : This single-axis form is *required* by functions + # like da.average (and np.average), if a 1d weights array is specified. + collapse_axes = collapse_axes[0] + try: data_result = aggregator.lazy_aggregate( - self.lazy_data(), axis=collapse_axis, **kwargs + self.lazy_data(), axis=collapse_axes, **kwargs ) except TypeError: # TypeError - when unexpected keywords passed through (such as @@ -3950,8 +3950,10 @@ def collapsed(self, coords, aggregator, **kwargs): unrolled_data = np.transpose(self.data, dims).reshape(new_shape) # Perform the same operation on the weights if applicable - if kwargs.get("weights") is not None: - weights = kwargs["weights"].view() + weights = kwargs.get("weights") + if weights is not None and weights.ndim > 1: + # Note: *don't* adjust 1d weights arrays, these have a special meaning for statistics functions. + weights = weights.view() kwargs["weights"] = np.transpose(weights, dims).reshape( new_shape ) diff --git a/lib/iris/tests/unit/common/resolve/__init__.py b/lib/iris/tests/unit/common/resolve/__init__.py new file mode 100644 index 00000000000..d0b189e59d3 --- /dev/null +++ b/lib/iris/tests/unit/common/resolve/__init__.py @@ -0,0 +1,6 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +"""Unit tests for the :mod:`iris.common.resolve` package.""" diff --git a/lib/iris/tests/unit/common/resolve/test_Resolve.py b/lib/iris/tests/unit/common/resolve/test_Resolve.py new file mode 100644 index 00000000000..94ec48de884 --- /dev/null +++ b/lib/iris/tests/unit/common/resolve/test_Resolve.py @@ -0,0 +1,4795 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Unit tests for the :class:`iris.common.resolve.Resolve`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from collections import namedtuple +from copy import deepcopy + +from cf_units import Unit +import numpy as np +import unittest.mock as mock +from unittest.mock import sentinel + +from iris.common.lenient import LENIENT +from iris.common.metadata import CubeMetadata +from iris.common.resolve import ( + Resolve, + _AuxCoverage, + _CategoryItems, + _DimCoverage, + _Item, + _PreparedItem, + _PreparedFactory, + _PreparedMetadata, +) +from iris.coords import DimCoord +from iris.cube import Cube + + +class Test___init__(tests.IrisTest): + def setUp(self): + target = "iris.common.resolve.Resolve.__call__" + self.m_call = mock.MagicMock(return_value=sentinel.return_value) + _ = self.patch(target, new=self.m_call) + + def _assert_members_none(self, resolve): + self.assertIsNone(resolve.lhs_cube_resolved) + self.assertIsNone(resolve.rhs_cube_resolved) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + self.assertIsNone(resolve.lhs_cube_dim_coverage) + self.assertIsNone(resolve.lhs_cube_aux_coverage) + self.assertIsNone(resolve.rhs_cube_dim_coverage) + self.assertIsNone(resolve.rhs_cube_aux_coverage) + self.assertIsNone(resolve.map_rhs_to_lhs) + self.assertIsNone(resolve.mapping) + self.assertIsNone(resolve.prepared_category) + self.assertIsNone(resolve.prepared_factories) + self.assertIsNone(resolve._broadcast_shape) + + def test_lhs_rhs_default(self): + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self._assert_members_none(resolve) + self.assertEqual(0, self.m_call.call_count) + + def test_lhs_rhs_provided(self): + m_lhs = sentinel.lhs + m_rhs = sentinel.rhs + resolve = Resolve(lhs=m_lhs, rhs=m_rhs) + # The lhs_cube and rhs_cube are only None due + # to __call__ being mocked. See Test___call__ + # for appropriate test coverage. + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self._assert_members_none(resolve) + self.assertEqual(1, self.m_call.call_count) + call_args = mock.call(m_lhs, m_rhs) + self.assertEqual(call_args, self.m_call.call_args) + + +class Test___call__(tests.IrisTest): + def setUp(self): + self.m_lhs = mock.MagicMock(spec=Cube) + self.m_rhs = mock.MagicMock(spec=Cube) + target = "iris.common.resolve.Resolve.{method}" + method = target.format(method="_metadata_resolve") + self.m_metadata_resolve = self.patch(method) + method = target.format(method="_metadata_coverage") + self.m_metadata_coverage = self.patch(method) + method = target.format(method="_metadata_mapping") + self.m_metadata_mapping = self.patch(method) + method = target.format(method="_metadata_prepare") + self.m_metadata_prepare = self.patch(method) + + def test_lhs_not_cube(self): + emsg = "'LHS' argument to be a 'Cube'" + with self.assertRaisesRegex(TypeError, emsg): + _ = Resolve(rhs=self.m_rhs) + + def test_rhs_not_cube(self): + emsg = "'RHS' argument to be a 'Cube'" + with self.assertRaisesRegex(TypeError, emsg): + _ = Resolve(lhs=self.m_lhs) + + def _assert_called_metadata_methods(self): + call_args = mock.call() + self.assertEqual(1, self.m_metadata_resolve.call_count) + self.assertEqual(call_args, self.m_metadata_resolve.call_args) + self.assertEqual(1, self.m_metadata_coverage.call_count) + self.assertEqual(call_args, self.m_metadata_coverage.call_args) + self.assertEqual(1, self.m_metadata_mapping.call_count) + self.assertEqual(call_args, self.m_metadata_mapping.call_args) + self.assertEqual(1, self.m_metadata_prepare.call_count) + self.assertEqual(call_args, self.m_metadata_prepare.call_args) + + def test_map_rhs_to_lhs__less_than(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 1 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertTrue(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + def test_map_rhs_to_lhs__equal(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 2 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertTrue(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + def test_map_lhs_to_rhs(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 3 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertFalse(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + +class Test__categorise_items(tests.IrisTest): + def setUp(self): + self.coord_dims = {} + # configure dim coords + coord = mock.Mock(metadata=sentinel.dim_metadata1) + self.dim_coords = [coord] + self.coord_dims[coord] = sentinel.dims1 + # configure aux and scalar coords + self.aux_coords = [] + pairs = [ + (sentinel.aux_metadata2, sentinel.dims2), + (sentinel.aux_metadata3, sentinel.dims3), + (sentinel.scalar_metadata4, None), + (sentinel.scalar_metadata5, None), + (sentinel.scalar_metadata6, None), + ] + for metadata, dims in pairs: + coord = mock.Mock(metadata=metadata) + self.aux_coords.append(coord) + self.coord_dims[coord] = dims + func = lambda coord: self.coord_dims[coord] + self.cube = mock.Mock( + aux_coords=self.aux_coords, + dim_coords=self.dim_coords, + coord_dims=func, + ) + + def test(self): + result = Resolve._categorise_items(self.cube) + self.assertIsInstance(result, _CategoryItems) + self.assertEqual(1, len(result.items_dim)) + # check dim coords + for item in result.items_dim: + self.assertIsInstance(item, _Item) + (coord,) = self.dim_coords + dims = self.coord_dims[coord] + expected = [_Item(metadata=coord.metadata, coord=coord, dims=dims)] + self.assertEqual(expected, result.items_dim) + # check aux coords + self.assertEqual(2, len(result.items_aux)) + for item in result.items_aux: + self.assertIsInstance(item, _Item) + expected_aux, expected_scalar = [], [] + for coord in self.aux_coords: + dims = self.coord_dims[coord] + item = _Item(metadata=coord.metadata, coord=coord, dims=dims) + if dims: + expected_aux.append(item) + else: + expected_scalar.append(item) + self.assertEqual(expected_aux, result.items_aux) + # check scalar coords + self.assertEqual(3, len(result.items_scalar)) + for item in result.items_scalar: + self.assertIsInstance(item, _Item) + self.assertEqual(expected_scalar, result.items_scalar) + + +class Test__metadata_resolve(tests.IrisTest): + def setUp(self): + self.target = "iris.common.resolve.Resolve._categorise_items" + self.m_lhs_cube = sentinel.lhs_cube + self.m_rhs_cube = sentinel.rhs_cube + + @staticmethod + def _create_items(pairs): + # this wrapper (hack) is necessary in order to support mocking + # the "name" method (callable) of the metadata, as "name" is already + # part of the mock API - this is always troublesome in mock-world. + Wrapper = namedtuple("Wrapper", ("name", "value")) + result = [] + for name, dims in pairs: + metadata = Wrapper(name=lambda: str(name), value=name) + coord = mock.Mock(metadata=metadata) + item = _Item(metadata=metadata, coord=coord, dims=dims) + result.append(item) + return result + + def test_metadata_same(self): + category = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + # configure dim coords + pairs = [(sentinel.dim_metadata1, sentinel.dims1)] + category.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims2), + (sentinel.aux_metadata2, sentinel.dims3), + ] + category.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + (sentinel.scalar_metadata3, None), + ] + category.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category, category) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # require to explicitly configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(mocker.call_count, 2) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category, resolve.lhs_cube_category) + self.assertEqual(category, resolve.rhs_cube_category) + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, resolve.lhs_cube_category_local) + self.assertEqual(expected, resolve.rhs_cube_category_local) + self.assertEqual(category, resolve.category_common) + + def test_metadata_overlap(self): + # configure the lhs cube category + category_lhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata1, sentinel.dims1), + (sentinel.dim_metadata2, sentinel.dims2), + ] + category_lhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims3), + (sentinel.aux_metadata2, sentinel.dims4), + ] + category_lhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + ] + category_lhs.items_scalar.extend(self._create_items(pairs)) + + # configure the rhs cube category + category_rhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + category_rhs.items_dim.append(category_lhs.items_dim[0]) + pairs = [(sentinel.dim_metadata200, sentinel.dims2)] + category_rhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + category_rhs.items_aux.append(category_lhs.items_aux[0]) + pairs = [(sentinel.aux_metadata200, sentinel.dims4)] + category_rhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + category_rhs.items_scalar.append(category_lhs.items_scalar[0]) + pairs = [(sentinel.scalar_metadata200, None)] + category_rhs.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category_lhs, category_rhs) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # require to explicitly configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(2, mocker.call_count) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category_lhs, resolve.lhs_cube_category) + self.assertEqual(category_rhs, resolve.rhs_cube_category) + + items_dim = [category_lhs.items_dim[1]] + items_aux = [category_lhs.items_aux[1]] + items_scalar = [category_lhs.items_scalar[1]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.lhs_cube_category_local) + + items_dim = [category_rhs.items_dim[1]] + items_aux = [category_rhs.items_aux[1]] + items_scalar = [category_rhs.items_scalar[1]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.rhs_cube_category_local) + + items_dim = [category_lhs.items_dim[0]] + items_aux = [category_lhs.items_aux[0]] + items_scalar = [category_lhs.items_scalar[0]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.category_common) + + def test_metadata_different(self): + # configure the lhs cube category + category_lhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata1, sentinel.dims1), + (sentinel.dim_metadata2, sentinel.dims2), + ] + category_lhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims3), + (sentinel.aux_metadata2, sentinel.dims4), + ] + category_lhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + ] + category_lhs.items_scalar.extend(self._create_items(pairs)) + + # configure the rhs cube category + category_rhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata100, sentinel.dims1), + (sentinel.dim_metadata200, sentinel.dims2), + ] + category_rhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata100, sentinel.dims3), + (sentinel.aux_metadata200, sentinel.dims4), + ] + category_rhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata100, None), + (sentinel.scalar_metadata200, None), + ] + category_rhs.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category_lhs, category_rhs) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # first require to explicitly lhs/rhs configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(2, mocker.call_count) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category_lhs, resolve.lhs_cube_category) + self.assertEqual(category_rhs, resolve.rhs_cube_category) + self.assertEqual(category_lhs, resolve.lhs_cube_category_local) + self.assertEqual(category_rhs, resolve.rhs_cube_category_local) + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, resolve.category_common) + + +class Test__dim_coverage(tests.IrisTest): + def setUp(self): + self.ndim = 4 + self.cube = mock.Mock(ndim=self.ndim) + self.items = [] + parts = [ + (sentinel.metadata0, sentinel.coord0, (0,)), + (sentinel.metadata1, sentinel.coord1, (1,)), + (sentinel.metadata2, sentinel.coord2, (2,)), + (sentinel.metadata3, sentinel.coord3, (3,)), + ] + column_parts = [x for x in zip(*parts)] + self.metadata, self.coords, self.dims = [list(x) for x in column_parts] + self.dims = [dim for dim, in self.dims] + for metadata, coord, dims in parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items.append(item) + + def test_coverage_no_local_no_common_all_free(self): + items = [] + common = [] + result = Resolve._dim_coverage(self.cube, items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + expected = [None] * self.ndim + self.assertEqual(expected, result.metadata) + self.assertEqual(expected, result.coords) + self.assertEqual([], result.dims_common) + self.assertEqual([], result.dims_local) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_free) + + def test_coverage_all_local_no_common_no_free(self): + common = [] + result = Resolve._dim_coverage(self.cube, self.items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.metadata, result.metadata) + self.assertEqual(self.coords, result.coords) + self.assertEqual([], result.dims_common) + self.assertEqual(self.dims, result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_no_local_all_common_no_free(self): + result = Resolve._dim_coverage(self.cube, self.items, self.metadata) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.metadata, result.metadata) + self.assertEqual(self.coords, result.coords) + self.assertEqual(self.dims, result.dims_common) + self.assertEqual([], result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_mixed(self): + common = [self.items[1].metadata, self.items[2].metadata] + self.items.pop(0) + self.items.pop(-1) + metadata, coord, dims = sentinel.metadata100, sentinel.coord100, (0,) + self.items.append(_Item(metadata=metadata, coord=coord, dims=dims)) + result = Resolve._dim_coverage(self.cube, self.items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + expected = [ + metadata, + self.items[0].metadata, + self.items[1].metadata, + None, + ] + self.assertEqual(expected, result.metadata) + expected = [coord, self.items[0].coord, self.items[1].coord, None] + self.assertEqual(expected, result.coords) + self.assertEqual([1, 2], result.dims_common) + self.assertEqual([0], result.dims_local) + self.assertEqual([3], result.dims_free) + + +class Test__aux_coverage(tests.IrisTest): + def setUp(self): + self.ndim = 4 + self.cube = mock.Mock(ndim=self.ndim) + # configure aux coords + self.items_aux = [] + aux_parts = [ + (sentinel.aux_metadata0, sentinel.aux_coord0, (0,)), + (sentinel.aux_metadata1, sentinel.aux_coord1, (1,)), + (sentinel.aux_metadata23, sentinel.aux_coord23, (2, 3)), + ] + column_aux_parts = [x for x in zip(*aux_parts)] + self.aux_metadata, self.aux_coords, self.aux_dims = [ + list(x) for x in column_aux_parts + ] + for metadata, coord, dims in aux_parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items_aux.append(item) + # configure scalar coords + self.items_scalar = [] + scalar_parts = [ + (sentinel.scalar_metadata0, sentinel.scalar_coord0, ()), + (sentinel.scalar_metadata1, sentinel.scalar_coord1, ()), + (sentinel.scalar_metadata2, sentinel.scalar_coord2, ()), + ] + column_scalar_parts = [x for x in zip(*scalar_parts)] + self.scalar_metadata, self.scalar_coords, self.scalar_dims = [ + list(x) for x in column_scalar_parts + ] + for metadata, coord, dims in scalar_parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items_scalar.append(item) + + def test_coverage_no_local_no_common_all_free(self): + items_aux, items_scalar = [], [] + common_aux, common_scalar = [], [] + result = Resolve._aux_coverage( + self.cube, items_aux, items_scalar, common_aux, common_scalar + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual([], result.common_items_aux) + self.assertEqual([], result.common_items_scalar) + self.assertEqual([], result.local_items_aux) + self.assertEqual([], result.local_items_scalar) + self.assertEqual([], result.dims_common) + self.assertEqual([], result.dims_local) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_free) + + def test_coverage_all_local_no_common_no_free(self): + common_aux, common_scalar = [], [] + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + common_aux, + common_scalar, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + expected = [] + self.assertEqual(expected, result.common_items_aux) + self.assertEqual(expected, result.common_items_scalar) + self.assertEqual(self.items_aux, result.local_items_aux) + self.assertEqual(self.items_scalar, result.local_items_scalar) + self.assertEqual([], result.dims_common) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_no_local_all_common_no_free(self): + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + self.aux_metadata, + self.scalar_metadata, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.items_aux, result.common_items_aux) + self.assertEqual(self.items_scalar, result.common_items_scalar) + self.assertEqual([], result.local_items_aux) + self.assertEqual([], result.local_items_scalar) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_common) + self.assertEqual([], result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_mixed(self): + common_aux = [self.items_aux[-1].metadata] + common_scalar = [self.items_scalar[1].metadata] + self.items_aux.pop(1) + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + common_aux, + common_scalar, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + expected = [self.items_aux[-1]] + self.assertEqual(expected, result.common_items_aux) + expected = [self.items_scalar[1]] + self.assertEqual(expected, result.common_items_scalar) + expected = [self.items_aux[0]] + self.assertEqual(expected, result.local_items_aux) + expected = [self.items_scalar[0], self.items_scalar[2]] + self.assertEqual(expected, result.local_items_scalar) + self.assertEqual([2, 3], result.dims_common) + self.assertEqual([0], result.dims_local) + self.assertEqual([1], result.dims_free) + + +class Test__metadata_coverage(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.m_lhs_cube = sentinel.lhs_cube + self.resolve.lhs_cube = self.m_lhs_cube + self.m_rhs_cube = sentinel.rhs_cube + self.resolve.rhs_cube = self.m_rhs_cube + self.m_items_dim_metadata = sentinel.items_dim_metadata + self.m_items_aux_metadata = sentinel.items_aux_metadata + self.m_items_scalar_metadata = sentinel.items_scalar_metadata + items_dim = [mock.Mock(metadata=self.m_items_dim_metadata)] + items_aux = [mock.Mock(metadata=self.m_items_aux_metadata)] + items_scalar = [mock.Mock(metadata=self.m_items_scalar_metadata)] + category = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.resolve.category_common = category + self.m_items_dim = sentinel.items_dim + self.m_items_aux = sentinel.items_aux + self.m_items_scalar = sentinel.items_scalar + category = _CategoryItems( + items_dim=self.m_items_dim, + items_aux=self.m_items_aux, + items_scalar=self.m_items_scalar, + ) + self.resolve.lhs_cube_category = category + self.resolve.rhs_cube_category = category + target = "iris.common.resolve.Resolve._dim_coverage" + self.m_lhs_cube_dim_coverage = sentinel.lhs_cube_dim_coverage + self.m_rhs_cube_dim_coverage = sentinel.rhs_cube_dim_coverage + side_effect = ( + self.m_lhs_cube_dim_coverage, + self.m_rhs_cube_dim_coverage, + ) + self.mocker_dim_coverage = self.patch(target, side_effect=side_effect) + target = "iris.common.resolve.Resolve._aux_coverage" + self.m_lhs_cube_aux_coverage = sentinel.lhs_cube_aux_coverage + self.m_rhs_cube_aux_coverage = sentinel.rhs_cube_aux_coverage + side_effect = ( + self.m_lhs_cube_aux_coverage, + self.m_rhs_cube_aux_coverage, + ) + self.mocker_aux_coverage = self.patch(target, side_effect=side_effect) + + def test(self): + self.resolve._metadata_coverage() + self.assertEqual(2, self.mocker_dim_coverage.call_count) + calls = [ + mock.call( + self.m_lhs_cube, self.m_items_dim, [self.m_items_dim_metadata] + ), + mock.call( + self.m_rhs_cube, self.m_items_dim, [self.m_items_dim_metadata] + ), + ] + self.assertEqual(calls, self.mocker_dim_coverage.call_args_list) + self.assertEqual(2, self.mocker_aux_coverage.call_count) + calls = [ + mock.call( + self.m_lhs_cube, + self.m_items_aux, + self.m_items_scalar, + [self.m_items_aux_metadata], + [self.m_items_scalar_metadata], + ), + mock.call( + self.m_rhs_cube, + self.m_items_aux, + self.m_items_scalar, + [self.m_items_aux_metadata], + [self.m_items_scalar_metadata], + ), + ] + self.assertEqual(calls, self.mocker_aux_coverage.call_args_list) + self.assertEqual( + self.m_lhs_cube_dim_coverage, self.resolve.lhs_cube_dim_coverage + ) + self.assertEqual( + self.m_rhs_cube_dim_coverage, self.resolve.rhs_cube_dim_coverage + ) + self.assertEqual( + self.m_lhs_cube_aux_coverage, self.resolve.lhs_cube_aux_coverage + ) + self.assertEqual( + self.m_rhs_cube_aux_coverage, self.resolve.rhs_cube_aux_coverage + ) + + +class Test__dim_mapping(tests.IrisTest): + def setUp(self): + self.ndim = 3 + Wrapper = namedtuple("Wrapper", ("name",)) + cube = Wrapper(name=lambda: sentinel.name) + self.src_coverage = _DimCoverage( + cube=cube, + metadata=[], + coords=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.tgt_coverage = _DimCoverage( + cube=cube, + metadata=[], + coords=None, + dims_common=[], + dims_local=None, + dims_free=None, + ) + self.metadata = [ + sentinel.metadata_0, + sentinel.metadata_1, + sentinel.metadata_2, + ] + self.dummy = [sentinel.dummy_0, sentinel.dummy_1, sentinel.dummy_2] + + def test_no_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.tgt_coverage.metadata.extend(self.dummy) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + self.assertEqual(dict(), result) + + def test_full_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = list(range(self.ndim)) + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1, 2: 2} + self.assertEqual(expected, result) + + def test_transpose_mapping(self): + self.src_coverage.metadata.extend(self.metadata[::-1]) + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = list(range(self.ndim)) + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 1: 1, 2: 0} + self.assertEqual(expected, result) + + def test_partial_mapping__transposed(self): + self.src_coverage.metadata.extend(self.metadata) + self.metadata[1] = sentinel.nope + self.tgt_coverage.metadata.extend(self.metadata[::-1]) + dims_common = [0, 2] + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 2: 0} + self.assertEqual(expected, result) + + def test_bad_metadata_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.metadata[0] = sentinel.bad + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = [0] + self.tgt_coverage.dims_common.extend(dims_common) + emsg = "Failed to map common dim coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + _ = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + + +class Test__aux_mapping(tests.IrisTest): + def setUp(self): + self.ndim = 3 + Wrapper = namedtuple("Wrapper", ("name",)) + cube = Wrapper(name=lambda: sentinel.name) + self.src_coverage = _AuxCoverage( + cube=cube, + common_items_aux=[], + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.tgt_coverage = _AuxCoverage( + cube=cube, + common_items_aux=[], + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.items = [ + _Item( + metadata=sentinel.metadata0, coord=sentinel.coord0, dims=[0] + ), + _Item( + metadata=sentinel.metadata1, coord=sentinel.coord1, dims=[1] + ), + _Item( + metadata=sentinel.metadata2, coord=sentinel.coord2, dims=[2] + ), + ] + + def _copy(self, items): + # Due to a bug in python 3.6.x, performing a deepcopy of a mock.sentinel + # will yield an object that is not equivalent to its parent, so this + # is a work-around until we drop support for python 3.6.x. + import sys + + version = sys.version_info + major, minor = version.major, version.minor + result = deepcopy(items) + if major == 3 and minor <= 6: + for i, item in enumerate(items): + result[i] = result[i]._replace(metadata=item.metadata) + return result + + def test_no_mapping(self): + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + self.assertEqual(dict(), result) + + def test_full_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + self.tgt_coverage.common_items_aux.extend(self.items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1, 2: 2} + self.assertEqual(expected, result) + + def test_transpose_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0].dims[0] = 2 + items[2].dims[0] = 0 + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 1: 1, 2: 0} + self.assertEqual(expected, result) + + def test_partial_mapping__transposed(self): + _ = self.items.pop(1) + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0].dims[0] = 2 + items[1].dims[0] = 0 + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 2: 0} + self.assertEqual(expected, result) + + def test_mapping__match_multiple_src_metadata(self): + items = self._copy(self.items) + _ = self.items.pop(1) + self.src_coverage.common_items_aux.extend(self.items) + items[1] = items[0] + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 2: 2} + self.assertEqual(expected, result) + + def test_mapping__skip_match_multiple_src_metadata(self): + items = self._copy(self.items) + _ = self.items.pop(1) + self.tgt_coverage.common_items_aux.extend(self.items) + items[1] = items[0]._replace(dims=[1]) + self.src_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {2: 2} + self.assertEqual(expected, result) + + def test_mapping__skip_different_rank(self): + items = self._copy(self.items) + self.src_coverage.common_items_aux.extend(self.items) + items[2] = items[2]._replace(dims=[1, 2]) + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1} + self.assertEqual(expected, result) + + def test_bad_metadata_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0] = items[0]._replace(metadata=sentinel.bad) + self.tgt_coverage.common_items_aux.extend(items) + emsg = "Failed to map common aux coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + _ = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + + +class Test_mapped(tests.IrisTest): + def test_mapping_none(self): + resolve = Resolve() + self.assertIsNone(resolve.mapping) + self.assertIsNone(resolve.mapped) + + def test_mapped__src_cube_lhs(self): + resolve = Resolve() + lhs = mock.Mock(ndim=2) + rhs = mock.Mock(ndim=3) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = False + resolve.mapping = {0: 0, 1: 1} + self.assertTrue(resolve.mapped) + + def test_mapped__src_cube_rhs(self): + resolve = Resolve() + lhs = mock.Mock(ndim=3) + rhs = mock.Mock(ndim=2) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = True + resolve.mapping = {0: 0, 1: 1} + self.assertTrue(resolve.mapped) + + def test_partial_mapping(self): + resolve = Resolve() + lhs = mock.Mock(ndim=3) + rhs = mock.Mock(ndim=2) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = True + resolve.mapping = {0: 0} + self.assertFalse(resolve.mapped) + + +class Test__free_mapping(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Wrapper", ("name", "ndim", "shape")) + self.src_dim_coverage = dict( + cube=None, + metadata=None, + coords=None, + dims_common=None, + dims_local=None, + dims_free=[], + ) + self.tgt_dim_coverage = deepcopy(self.src_dim_coverage) + self.src_aux_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=[], + ) + self.tgt_aux_coverage = deepcopy(self.src_aux_coverage) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.mapping = {} + + def _make_args(self): + args = dict( + src_dim_coverage=_DimCoverage(**self.src_dim_coverage), + tgt_dim_coverage=_DimCoverage(**self.tgt_dim_coverage), + src_aux_coverage=_AuxCoverage(**self.src_aux_coverage), + tgt_aux_coverage=_AuxCoverage(**self.tgt_aux_coverage), + ) + return args + + def test_mapping_no_dims_free(self): + ndim = 4 + shape = tuple(range(ndim)) + cube = self.Cube(name=lambda: "name", ndim=ndim, shape=shape) + self.src_dim_coverage["cube"] = cube + self.tgt_dim_coverage["cube"] = cube + args = self._make_args() + emsg = "Insufficient matching coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + def _make_coverage(self, name, shape, dims_free): + if name == "src": + dim_coverage = self.src_dim_coverage + aux_coverage = self.src_aux_coverage + else: + dim_coverage = self.tgt_dim_coverage + aux_coverage = self.tgt_aux_coverage + ndim = len(shape) + cube = self.Cube(name=lambda: name, ndim=ndim, shape=shape) + dim_coverage["cube"] = cube + dim_coverage["dims_free"].extend(dims_free) + aux_coverage["cube"] = cube + aux_coverage["dims_free"].extend(dims_free) + + def test_mapping_src_free_to_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state f l c l state f c f + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 4 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (1, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 1 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 1 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->1 1->2 2->3 + src_shape = (1, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 1, 1: 2, 2: 3} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state f f c f state f c f + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 4 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (1, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 1 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (2, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 1 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (1, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt__fail(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 5 + # state f f c f state f c f + # coord d d d a coord a d d + # fail ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->? + src_shape = (2, 3, 5) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + emsg = "Insufficient matching coordinate metadata to resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + def test_mapping_tgt_free_to_src_local(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 1 3 2 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 1, 3, 2) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 1 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 1) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 1 3 1 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->1 1->2 2->3 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 1, 3, 1) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 1, 1: 2, 2: 3} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_no_free__fail(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 5 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # fail ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->? + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 5) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + emsg = "Insufficient matching coordinate metadata to resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + +class Test__src_cube(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._src_cube) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._src_cube) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube + + +class Test__src_cube_position(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.assertEqual("RHS", self.resolve._src_cube_position) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.assertEqual("LHS", self.resolve._src_cube_position) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_position + + +class Test__src_cube_resolved__getter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._src_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._src_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_resolved + + +class Test__src_cube_resolved__setter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve._src_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.rhs_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve._src_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.lhs_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_resolved = self.expected + + +class Test__tgt_cube(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.rhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.lhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube + + +class Test__tgt_cube_position(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.assertEqual("RHS", self.resolve._tgt_cube_position) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.assertEqual("LHS", self.resolve._tgt_cube_position) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_position + + +class Test__tgt_cube_resolved__getter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.rhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.lhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_resolved + + +class Test__tgt_cube_resolved__setter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve._tgt_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.rhs_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve._tgt_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.lhs_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_resolved = self.expected + + +class Test_shape(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_no_shape(self): + self.assertIsNone(self.resolve.shape) + + def test_shape(self): + expected = sentinel.shape + self.resolve._broadcast_shape = expected + self.assertEqual(expected, self.resolve.shape) + + +class Test__as_compatible_cubes(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple( + "Wrapper", + ( + "name", + "ndim", + "shape", + "metadata", + "core_data", + "coord_dims", + "dim_coords", + "aux_coords", + "aux_factories", + ), + ) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.mapping = {} + self.mocker = self.patch("iris.cube.Cube") + self.args = dict( + name=None, + ndim=None, + shape=None, + metadata=None, + core_data=None, + coord_dims=None, + dim_coords=None, + aux_coords=None, + aux_factories=None, + ) + + def _make_cube(self, name, shape, transpose_shape=None): + self.args["name"] = lambda: name + ndim = len(shape) + self.args["ndim"] = ndim + self.args["shape"] = shape + if name == "src": + self.args["metadata"] = sentinel.metadata + self.reshape = sentinel.reshape + m_reshape = mock.Mock(return_value=self.reshape) + self.transpose = mock.Mock( + shape=transpose_shape, reshape=m_reshape + ) + m_transpose = mock.Mock(return_value=self.transpose) + self.data = mock.Mock( + shape=shape, transpose=m_transpose, reshape=m_reshape + ) + m_copy = mock.Mock(return_value=self.data) + m_core_data = mock.Mock(copy=m_copy) + self.args["core_data"] = mock.Mock(return_value=m_core_data) + self.args["coord_dims"] = mock.Mock(side_effect=([0], [ndim - 1])) + self.dim_coord = sentinel.dim_coord + self.aux_coord = sentinel.aux_coord + self.aux_factory = sentinel.aux_factory + self.args["dim_coords"] = [self.dim_coord] + self.args["aux_coords"] = [self.aux_coord] + self.args["aux_factories"] = [self.aux_factory] + cube = self.Cube(**self.args) + self.resolve.rhs_cube = cube + self.cube = mock.Mock() + self.mocker.return_value = self.cube + else: + cube = self.Cube(**self.args) + self.resolve.lhs_cube = cube + + def test_incomplete_src_to_tgt_mapping__fail(self): + src_shape = (1, 2) + self._make_cube("src", src_shape) + tgt_shape = (3, 4) + self._make_cube("tgt", tgt_shape) + with self.assertRaises(AssertionError): + self.resolve._as_compatible_cubes() + + def test_incompatible_shapes__fail(self): + # key: (state) c=common, f=free + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 2 3 4 shape 2 3 5 + # state f c c c state c c c + # fail ^ fail ^ + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + src_shape = (2, 3, 5) + self._make_cube("src", src_shape) + tgt_shape = (2, 2, 3, 4) + self._make_cube("tgt", tgt_shape) + self.resolve.mapping = {0: 1, 1: 2, 2: 3} + emsg = "Cannot resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._as_compatible_cubes() + + def test_incompatible_shapes__fail_broadcast(self): + # key: (state) c=common, f=free + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 5 + # state f c c c state c c c + # fail ^ fail ^ + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 3, 5) + self._make_cube("src", src_shape) + tgt_shape = (2, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + self.resolve.mapping = {0: 3, 1: 2, 2: 1} + emsg = "Cannot resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._as_compatible_cubes() + + def _check_compatible(self, broadcast_shape): + self.assertEqual( + self.resolve.lhs_cube, self.resolve._tgt_cube_resolved + ) + self.assertEqual(self.cube, self.resolve._src_cube_resolved) + self.assertEqual(broadcast_shape, self.resolve._broadcast_shape) + self.assertEqual(1, self.mocker.call_count) + self.assertEqual(self.args["metadata"], self.cube.metadata) + self.assertEqual(2, self.resolve.rhs_cube.coord_dims.call_count) + self.assertEqual( + [mock.call(self.dim_coord), mock.call(self.aux_coord)], + self.resolve.rhs_cube.coord_dims.call_args_list, + ) + self.assertEqual(1, self.cube.add_dim_coord.call_count) + self.assertEqual( + [mock.call(self.dim_coord, [self.resolve.mapping[0]])], + self.cube.add_dim_coord.call_args_list, + ) + self.assertEqual(1, self.cube.add_aux_coord.call_count) + self.assertEqual( + [mock.call(self.aux_coord, [self.resolve.mapping[2]])], + self.cube.add_aux_coord.call_args_list, + ) + self.assertEqual(1, self.cube.add_aux_factory.call_count) + self.assertEqual( + [mock.call(self.aux_factory)], + self.cube.add_aux_factory.call_args_list, + ) + + def test_compatible(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 4 3 2 + # state c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + src_shape = (4, 3, 2) + self._make_cube("src", src_shape) + tgt_shape = (4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 0, 1: 1, 2: 2} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual([mock.call(self.data)], self.mocker.call_args_list) + + def test_compatible__transpose(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 2 3 4 + # state c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->2, 1->1, 2->0 + src_shape = (2, 3, 4) + self._make_cube("src", src_shape, transpose_shape=(4, 3, 2)) + tgt_shape = (4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 2, 1: 1, 2: 0} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual( + [mock.call(self.transpose)], self.mocker.call_args_list + ) + + def test_compatible__reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + src_shape = (4, 3, 2) + self._make_cube("src", src_shape) + tgt_shape = (5, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + src_shape)], self.data.reshape.call_args_list + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + def test_compatible__transpose_reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 2 3 4 + # state f c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 3, 4) + transpose_shape = (4, 3, 2) + self._make_cube("src", src_shape, transpose_shape=transpose_shape) + tgt_shape = (5, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 3, 1: 2, 2: 1} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + transpose_shape)], + self.data.reshape.call_args_list, + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + def test_compatible__broadcast(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 1 3 2 shape 4 1 2 + # state c c c state c c c + # coord d a + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + src_shape = (4, 1, 2) + self._make_cube("src", src_shape) + tgt_shape = (1, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 0, 1: 1, 2: 2} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=(4, 3, 2)) + self.assertEqual([mock.call(self.data)], self.mocker.call_args_list) + + def test_compatible__broadcast_transpose_reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 1 3 2 shape 2 1 4 + # state f c c c state c c c + # coord d a + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 1, 4) + transpose_shape = (4, 1, 2) + self._make_cube("src", src_shape) + tgt_shape = (5, 1, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 3, 1: 2, 2: 1} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=(5, 4, 3, 2)) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + transpose_shape)], + self.data.reshape.call_args_list, + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + +class Test__metadata_mapping(tests.IrisTest): + def setUp(self): + self.ndim = sentinel.ndim + self.src_cube = mock.Mock(ndim=self.ndim) + self.src_dim_coverage = mock.Mock(dims_free=[]) + self.src_aux_coverage = mock.Mock(dims_free=[]) + self.tgt_cube = mock.Mock(ndim=self.ndim) + self.tgt_dim_coverage = mock.Mock(dims_free=[]) + self.tgt_aux_coverage = mock.Mock(dims_free=[]) + self.resolve = Resolve() + self.map_rhs_to_lhs = True + self.resolve.map_rhs_to_lhs = self.map_rhs_to_lhs + self.resolve.rhs_cube = self.src_cube + self.resolve.rhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.lhs_cube = self.tgt_cube + self.resolve.lhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.tgt_aux_coverage + self.resolve.mapping = {} + self.shape = sentinel.shape + self.resolve._broadcast_shape = self.shape + self.resolve._src_cube_resolved = mock.Mock(shape=self.shape) + self.resolve._tgt_cube_resolved = mock.Mock(shape=self.shape) + self.m_dim_mapping = self.patch( + "iris.common.resolve.Resolve._dim_mapping", return_value={} + ) + self.m_aux_mapping = self.patch( + "iris.common.resolve.Resolve._aux_mapping", return_value={} + ) + self.m_free_mapping = self.patch( + "iris.common.resolve.Resolve._free_mapping" + ) + self.m_as_compatible_cubes = self.patch( + "iris.common.resolve.Resolve._as_compatible_cubes" + ) + self.mapping = {0: 1, 1: 2, 2: 3} + + def test_mapped__dim_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d d d coord d d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = self.mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(0, self.m_aux_mapping.call_count) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__aux_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord a a a coord a a a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.src_cube.ndim = 3 + self.m_aux_mapping.return_value = self.mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_and_aux_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d a d coord d a d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + dim_mapping = {0: 1, 2: 3} + aux_mapping = {1: 2} + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + self.m_aux_mapping.return_value = aux_mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_coords_and_free_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l f c c state f c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + dim_mapping = {1: 2, 2: 3} + free_mapping = {0: 1} + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + side_effect = lambda a, b, c, d: self.resolve.mapping.update( + free_mapping + ) + self.m_free_mapping.side_effect = side_effect + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(1, self.m_free_mapping.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.tgt_dim_coverage, + self.src_aux_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_free_mapping.call_args_list) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_coords_with_broadcast_flip(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 4 dims 0 1 2 4 + # shape 1 4 3 2 shape 5 4 3 2 + # state c c c c state c c c c + # coord d d d d coord d d d d + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2, 3->3 + mapping = {0: 0, 1: 1, 2: 2, 3: 3} + self.src_cube.ndim = 4 + self.tgt_cube.ndim = 4 + self.m_dim_mapping.return_value = mapping + broadcast_shape = (5, 4, 3, 2) + self.resolve._broadcast_shape = broadcast_shape + self.resolve._src_cube_resolved.shape = broadcast_shape + self.resolve._tgt_cube_resolved.shape = (1, 4, 3, 2) + self.resolve._metadata_mapping() + self.assertEqual(mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(0, self.m_aux_mapping.call_count) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(2, self.m_as_compatible_cubes.call_count) + self.assertEqual(not self.map_rhs_to_lhs, self.resolve.map_rhs_to_lhs) + + def test_mapped__dim_coords_free_flip_with_free_flip(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 4 3 2 + # state f f c state l l c + # coord d coord d d d + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + dim_mapping = {2: 2} + free_mapping = {0: 0, 1: 1} + mapping = {0: 0, 1: 1, 2: 2} + self.src_cube.ndim = 3 + self.tgt_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + side_effect = lambda a, b, c, d: self.resolve.mapping.update( + free_mapping + ) + self.m_free_mapping.side_effect = side_effect + self.tgt_dim_coverage.dims_free = [0, 1] + self.tgt_aux_coverage.dims_free = [0, 1] + self.resolve._metadata_mapping() + self.assertEqual(mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(1, self.m_free_mapping.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.tgt_dim_coverage, + self.src_aux_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_free_mapping.call_args_list) + self.assertEqual(2, self.m_as_compatible_cubes.call_count) + + +class Test__prepare_common_dim_payload(tests.IrisTest): + def setUp(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l c c c state c c c + # coord d d d coord d d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.points = (sentinel.points_0, sentinel.points_1, sentinel.points_2) + self.bounds = (sentinel.bounds_0, sentinel.bounds_1, sentinel.bounds_2) + self.pb_0 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[0])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[0])), + ) + self.pb_1 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[1])), + None, + ) + self.pb_2 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[2])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[2])), + ) + side_effect = (self.pb_0, self.pb_1, self.pb_2) + self.m_prepare_points_and_bounds = self.patch( + "iris.common.resolve.Resolve._prepare_points_and_bounds", + side_effect=side_effect, + ) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = self.mapping + self.metadata_combined = ( + sentinel.combined_0, + sentinel.combined_1, + sentinel.combined_2, + ) + self.src_metadata = mock.Mock( + combine=mock.Mock(side_effect=self.metadata_combined) + ) + metadata = [self.src_metadata] * len(self.mapping) + self.src_coords = [ + sentinel.src_coord_0, + sentinel.src_coord_1, + sentinel.src_coord_2, + ] + self.src_dims_common = [0, 1, 2] + self.container = DimCoord + self.src_dim_coverage = _DimCoverage( + cube=None, + metadata=metadata, + coords=self.src_coords, + dims_common=self.src_dims_common, + dims_local=[], + dims_free=[], + ) + self.tgt_metadata = [ + sentinel.tgt_metadata_0, + sentinel.tgt_metadata_1, + sentinel.tgt_metadata_2, + sentinel.tgt_metadata_3, + ] + self.tgt_coords = [ + sentinel.tgt_coord_0, + sentinel.tgt_coord_1, + sentinel.tgt_coord_2, + sentinel.tgt_coord_3, + ] + self.tgt_dims_common = [1, 2, 3] + self.tgt_dim_coverage = _DimCoverage( + cube=None, + metadata=self.tgt_metadata, + coords=self.tgt_coords, + dims_common=self.tgt_dims_common, + dims_local=[], + dims_free=[], + ) + + def _check(self, ignore_mismatch=None, bad_points=None): + if bad_points is None: + bad_points = False + self.resolve._prepare_common_dim_payload( + self.src_dim_coverage, + self.tgt_dim_coverage, + ignore_mismatch=ignore_mismatch, + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + if not bad_points: + self.assertEqual(3, len(self.resolve.prepared_category.items_dim)) + expected = [ + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[0], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[0]], + ), + points=self.points[0], + bounds=self.bounds[0], + dims=(self.mapping[0],), + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[1], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[1]], + ), + points=self.points[1], + bounds=None, + dims=(self.mapping[1],), + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[2], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[2]], + ), + points=self.points[2], + bounds=self.bounds[2], + dims=(self.mapping[2],), + container=self.container, + ), + ] + self.assertEqual( + expected, self.resolve.prepared_category.items_dim + ) + else: + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(3, self.m_prepare_points_and_bounds.call_count) + if ignore_mismatch is None: + ignore_mismatch = False + expected = [ + mock.call( + self.src_coords[0], + self.tgt_coords[self.mapping[0]], + 0, + 1, + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[1], + self.tgt_coords[self.mapping[1]], + 1, + 2, + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[2], + self.tgt_coords[self.mapping[2]], + 2, + 3, + ignore_mismatch=ignore_mismatch, + ), + ] + self.assertEqual( + expected, self.m_prepare_points_and_bounds.call_args_list + ) + if not bad_points: + self.assertEqual(3, self.src_metadata.combine.call_count) + expected = [ + mock.call(metadata) for metadata in self.tgt_metadata[1:] + ] + self.assertEqual( + expected, self.src_metadata.combine.call_args_list + ) + + def test__default_ignore_mismatch(self): + self._check() + + def test__not_ignore_mismatch(self): + self._check(ignore_mismatch=False) + + def test__ignore_mismatch(self): + self._check(ignore_mismatch=True) + + def test__bad_points(self): + side_effect = [(None, None)] * len(self.mapping) + self.m_prepare_points_and_bounds.side_effect = side_effect + self._check(bad_points=True) + + +class Test__prepare_common_aux_payload(tests.IrisTest): + def setUp(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l c c c state c c c + # coord a a a coord a a a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.points = (sentinel.points_0, sentinel.points_1, sentinel.points_2) + self.bounds = (sentinel.bounds_0, sentinel.bounds_1, sentinel.bounds_2) + self.pb_0 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[0])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[0])), + ) + self.pb_1 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[1])), + None, + ) + self.pb_2 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[2])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[2])), + ) + side_effect = (self.pb_0, self.pb_1, self.pb_2) + self.m_prepare_points_and_bounds = self.patch( + "iris.common.resolve.Resolve._prepare_points_and_bounds", + side_effect=side_effect, + ) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = self.mapping + self.resolve.map_rhs_to_lhs = True + self.metadata_combined = ( + sentinel.combined_0, + sentinel.combined_1, + sentinel.combined_2, + ) + self.src_metadata = [ + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[0]) + ), + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[1]) + ), + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[2]) + ), + ] + self.src_coords = [ + sentinel.src_coord_0, + sentinel.src_coord_1, + sentinel.src_coord_2, + ] + self.src_dims = [(dim,) for dim in self.mapping.keys()] + self.src_common_items = [ + _Item(*item) + for item in zip(self.src_metadata, self.src_coords, self.src_dims) + ] + self.tgt_metadata = [sentinel.tgt_metadata_0] + self.src_metadata + self.tgt_coords = [ + sentinel.tgt_coord_0, + sentinel.tgt_coord_1, + sentinel.tgt_coord_2, + sentinel.tgt_coord_3, + ] + self.tgt_dims = [None] + [(dim,) for dim in self.mapping.values()] + self.tgt_common_items = [ + _Item(*item) + for item in zip(self.tgt_metadata, self.tgt_coords, self.tgt_dims) + ] + self.container = type(self.src_coords[0]) + + def _check(self, ignore_mismatch=None, bad_points=None): + if bad_points is None: + bad_points = False + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, + self.tgt_common_items, + prepared_items, + ignore_mismatch=ignore_mismatch, + ) + if not bad_points: + self.assertEqual(3, len(prepared_items)) + expected = [ + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[0], + src=self.src_metadata[0], + tgt=self.tgt_metadata[self.mapping[0]], + ), + points=self.points[0], + bounds=self.bounds[0], + dims=self.tgt_dims[self.mapping[0]], + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[1], + src=self.src_metadata[1], + tgt=self.tgt_metadata[self.mapping[1]], + ), + points=self.points[1], + bounds=None, + dims=self.tgt_dims[self.mapping[1]], + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[2], + src=self.src_metadata[2], + tgt=self.tgt_metadata[self.mapping[2]], + ), + points=self.points[2], + bounds=self.bounds[2], + dims=self.tgt_dims[self.mapping[2]], + container=self.container, + ), + ] + self.assertEqual(expected, prepared_items) + else: + self.assertEqual(0, len(prepared_items)) + self.assertEqual(3, self.m_prepare_points_and_bounds.call_count) + if ignore_mismatch is None: + ignore_mismatch = False + expected = [ + mock.call( + self.src_coords[0], + self.tgt_coords[self.mapping[0]], + self.src_dims[0], + self.tgt_dims[self.mapping[0]], + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[1], + self.tgt_coords[self.mapping[1]], + self.src_dims[1], + self.tgt_dims[self.mapping[1]], + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[2], + self.tgt_coords[self.mapping[2]], + self.src_dims[2], + self.tgt_dims[self.mapping[2]], + ignore_mismatch=ignore_mismatch, + ), + ] + self.assertEqual( + expected, self.m_prepare_points_and_bounds.call_args_list + ) + if not bad_points: + for src_metadata, tgt_metadata in zip( + self.src_metadata, self.tgt_metadata[1:] + ): + self.assertEqual(1, src_metadata.combine.call_count) + expected = [mock.call(tgt_metadata)] + self.assertEqual(expected, src_metadata.combine.call_args_list) + + def test__default_ignore_mismatch(self): + self._check() + + def test__not_ignore_mismatch(self): + self._check(ignore_mismatch=False) + + def test__ignore_mismatch(self): + self._check(ignore_mismatch=True) + + def test__bad_points(self): + side_effect = [(None, None)] * len(self.mapping) + self.m_prepare_points_and_bounds.side_effect = side_effect + self._check(bad_points=True) + + def test__no_tgt_metadata_match(self): + item = self.tgt_common_items[0] + tgt_common_items = [item] * len(self.tgt_common_items) + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, tgt_common_items, prepared_items + ) + self.assertEqual(0, len(prepared_items)) + + def test__multi_tgt_metadata_match(self): + item = self.tgt_common_items[1] + tgt_common_items = [item] * len(self.tgt_common_items) + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, tgt_common_items, prepared_items + ) + self.assertEqual(0, len(prepared_items)) + + +class Test__prepare_points_and_bounds(tests.IrisTest): + def setUp(self): + self.Coord = namedtuple( + "Coord", + [ + "name", + "points", + "bounds", + "metadata", + "ndim", + "shape", + "has_bounds", + ], + ) + self.Cube = namedtuple("Cube", ["name", "shape"]) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.src_name = sentinel.src_name + self.src_points = sentinel.src_points + self.src_bounds = sentinel.src_bounds + self.src_metadata = sentinel.src_metadata + self.src_items = dict( + name=lambda: self.src_name, + points=self.src_points, + bounds=self.src_bounds, + metadata=self.src_metadata, + ndim=None, + shape=None, + has_bounds=None, + ) + self.tgt_name = sentinel.tgt_name + self.tgt_points = sentinel.tgt_points + self.tgt_bounds = sentinel.tgt_bounds + self.tgt_metadata = sentinel.tgt_metadata + self.tgt_items = dict( + name=lambda: self.tgt_name, + points=self.tgt_points, + bounds=self.tgt_bounds, + metadata=self.tgt_metadata, + ndim=None, + shape=None, + has_bounds=None, + ) + self.m_array_equal = self.patch( + "iris.util.array_equal", side_effect=(True, True) + ) + + def test_coord_ndim_unequal__tgt_ndim_greater(self): + self.src_items["ndim"] = 1 + src_coord = self.Coord(**self.src_items) + self.tgt_items["ndim"] = 10 + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims=None, tgt_dims=None + ) + self.assertEqual(self.tgt_points, points) + self.assertEqual(self.tgt_bounds, bounds) + + def test_coord_ndim_unequal__src_ndim_greater(self): + self.src_items["ndim"] = 10 + src_coord = self.Coord(**self.src_items) + self.tgt_items["ndim"] = 1 + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims=None, tgt_dims=None + ) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_src_broadcasting(self): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 9 9 shape 1 9 + # state c c state c c + # coord x-x coord x-x + # bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = (1, 9) + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube(name=None, shape=src_shape) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = broadcast_shape + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube(name=None, shape=tgt_shape) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + self.assertEqual(self.tgt_points, points) + self.assertEqual(self.tgt_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_tgt_broadcasting(self): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 1 9 shape 9 9 + # state c c state c c + # coord x-x coord x-x + # bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = broadcast_shape + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube(name=None, shape=src_shape) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = (1, 9) + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube(name=None, shape=tgt_shape) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_unsupported_broadcasting( + self, + ): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 1 9 shape 9 1 + # state c c state c c + # coord x-x coord x-x + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = (9, 1) + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube( + name=lambda: sentinel.src_cube, shape=src_shape + ) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = (1, 9) + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube( + name=lambda: sentinel.tgt_cube, shape=tgt_shape + ) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + emsg = "Cannot broadcast" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + + def _populate( + self, src_points, tgt_points, src_bounds=None, tgt_bounds=None + ): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state f c state f c + # coord x coord x + # + # src-to-tgt mapping: + # 0->0, 1->1 + shape = (2, 3) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.Cube( + name=lambda: sentinel.src_cube, shape=None + ) + self.resolve.lhs_cube = self.Cube( + name=lambda: sentinel.tgt_cube, shape=None + ) + ndim = 1 + src_dims = 1 + self.src_items["ndim"] = ndim + self.src_items["shape"] = (shape[src_dims],) + self.src_items["points"] = src_points + self.src_items["bounds"] = src_bounds + self.src_items["has_bounds"] = lambda: src_bounds is not None + src_coord = self.Coord(**self.src_items) + tgt_dims = 1 + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = (shape[mapping[tgt_dims]],) + self.tgt_items["points"] = tgt_points + self.tgt_items["bounds"] = tgt_bounds + self.tgt_items["has_bounds"] = lambda: tgt_bounds is not None + tgt_coord = self.Coord(**self.tgt_items) + args = dict( + src_coord=src_coord, + tgt_coord=tgt_coord, + src_dims=src_dims, + tgt_dims=tgt_dims, + ) + return args + + def test_coord_ndim_and_shape_equal__points_equal_with_no_bounds(self): + args = self._populate(self.src_points, self.src_points) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertIsNone(bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_src_bounds_only( + self, + ): + args = self._populate( + self.src_points, self.src_points, src_bounds=self.src_bounds + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_tgt_bounds_only( + self, + ): + args = self._populate( + self.src_points, self.src_points, tgt_bounds=self.tgt_bounds + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.tgt_bounds, bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_src_bounds_only_strict( + self, + ): + args = self._populate( + self.src_points, self.src_points, src_bounds=self.src_bounds + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_tgt_bounds_only_strict( + self, + ): + args = self._populate( + self.src_points, self.src_points, tgt_bounds=self.tgt_bounds + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.tgt_name} has bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_equal(self): + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.src_bounds, + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + self.assertEqual(2, self.m_array_equal.call_count) + expected = [ + mock.call(self.src_points, self.src_points, withnans=True), + mock.call(self.src_bounds, self.src_bounds, withnans=True), + ] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + emsg = f"Coordinate {self.src_name} has different bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different_ignore_mismatch( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + points, bounds = self.resolve._prepare_points_and_bounds( + **args, ignore_mismatch=True + ) + self.assertEqual(self.src_points, points) + self.assertIsNone(bounds) + self.assertEqual(2, self.m_array_equal.call_count) + expected = [ + mock.call(self.src_points, self.src_points, withnans=True), + mock.call(self.src_bounds, self.tgt_bounds, withnans=True), + ] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different_strict( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has different bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_different(self): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + emsg = f"Coordinate {self.src_name} has different points" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_different_ignore_mismatch( + self, + ): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + points, bounds = self.resolve._prepare_points_and_bounds( + **args, ignore_mismatch=True + ) + self.assertIsNone(points) + self.assertIsNone(bounds) + + def test_coord_ndim_and_shape_equal__points_different_strict(self): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has different points" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + +class Test__create_prepared_item(tests.IrisTest): + def setUp(self): + Coord = namedtuple("Coord", ["points", "bounds"]) + self.points_value = sentinel.points + self.points = mock.Mock(copy=mock.Mock(return_value=self.points_value)) + self.bounds_value = sentinel.bounds + self.bounds = mock.Mock(copy=mock.Mock(return_value=self.bounds_value)) + self.coord = Coord(points=self.points, bounds=self.bounds) + self.container = type(self.coord) + self.combined = sentinel.combined + self.src = mock.Mock(combine=mock.Mock(return_value=self.combined)) + self.tgt = sentinel.tgt + + def _check(self, src=None, tgt=None): + dims = 0 + if src is not None and tgt is not None: + combined = self.combined + else: + combined = src or tgt + result = Resolve._create_prepared_item( + self.coord, dims, src_metadata=src, tgt_metadata=tgt + ) + self.assertIsInstance(result, _PreparedItem) + self.assertIsInstance(result.metadata, _PreparedMetadata) + expected = _PreparedMetadata(combined=combined, src=src, tgt=tgt) + self.assertEqual(expected, result.metadata) + self.assertEqual(self.points_value, result.points) + self.assertEqual(1, self.points.copy.call_count) + self.assertEqual([mock.call()], self.points.copy.call_args_list) + self.assertEqual(self.bounds_value, result.bounds) + self.assertEqual(1, self.bounds.copy.call_count) + self.assertEqual([mock.call()], self.bounds.copy.call_args_list) + self.assertEqual((dims,), result.dims) + self.assertEqual(self.container, result.container) + + def test__no_metadata(self): + self._check() + + def test__src_metadata_only(self): + self._check(src=self.src) + + def test__tgt_metadata_only(self): + self._check(tgt=self.tgt) + + def test__combine_metadata(self): + self._check(src=self.src, tgt=self.tgt) + + +class Test__prepare_local_payload_dim(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.resolve.map_rhs_to_lhs = True + self.src_coverage = dict( + cube=None, + metadata=[], + coords=[], + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.prepared_item = sentinel.prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + return_value=self.prepared_item, + ) + + def test_src_no_local_with_tgt_no_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_no_local_with_tgt_no_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.src_coverage["dims_local"] = (1,) + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["dims_local"] = (1,) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.src_coverage["dims_local"] = (1,) + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["dims_local"] = (1,) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_dim = 1 + self.src_coverage["dims_local"] = (src_dim,) + src_metadata = sentinel.src_metadata + self.src_coverage["metadata"] = [None, src_metadata] + src_coord = sentinel.src_coord + self.src_coverage["coords"] = [None, src_coord] + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [ + mock.call(src_coord, mapping[src_dim], src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_free__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_dim = 1 + self.src_coverage["dims_local"] = (src_dim,) + src_metadata = sentinel.src_metadata + self.src_coverage["metadata"] = [None, src_metadata] + src_coord = sentinel.src_coord + self.src_coverage["coords"] = [None, src_coord] + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_free_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord d d coord d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_dim = 1 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [None, tgt_metadata] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [None, tgt_coord] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_free_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord d d coord d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_dim = 1 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [None, tgt_metadata] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [None, tgt_coord] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_no_local_with_tgt_local__extra_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2 + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_dim = 0 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [tgt_metadata, None, None] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [tgt_coord, None, None] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__extra_dims_strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2 + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_dim = 0 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [tgt_metadata, None, None] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [tgt_coord, None, None] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test__prepare_local_payload_aux(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.resolve.map_rhs_to_lhs = True + self.src_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=[], + local_items_scalar=None, + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.src_prepared_item = sentinel.src_prepared_item + self.tgt_prepared_item = sentinel.tgt_prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + side_effect=(self.src_prepared_item, self.tgt_prepared_item), + ) + + def test_src_no_local_with_tgt_no_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_no_local_with_tgt_no_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_local_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [ + mock.call(src_coord, tgt_dims, src_metadata=src_metadata), + mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_local_with_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + self.src_coverage["dims_local"].extend(src_dims) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(src_coord, src_dims, src_metadata=src_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_free__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + self.src_coverage["dims_local"].extend(src_dims) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_free_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord a a coord a + # + # src-to-tgt mapping: + # 0->0, 1->1 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_free_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord a a coord a + # + # src-to-tgt mapping: + # 0->0, 1->1 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_no_local_with_tgt_local__extra_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord a a a coord a a + # + # src-to-tgt mapping: + # 0->1, 1->2 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (0,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__extra_dims_strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord a a a coord a a + # + # src-to-tgt mapping: + # 0->1, 1->2 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (0,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=True): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test__prepare_local_payload_scalar(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.src_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=[], + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.src_prepared_item = sentinel.src_prepared_item + self.tgt_prepared_item = sentinel.tgt_prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + side_effect=(self.src_prepared_item, self.tgt_prepared_item), + ) + self.src_dims = () + self.tgt_dims = () + + def test_src_no_local_with_tgt_no_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_no_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_no_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_no_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_no_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_local(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__strict(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_local__src_scalar_cube(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__src_scalar_cube_strict(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata), + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata), + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + +class Test__prepare_local_payload(tests.IrisTest): + def test(self): + src_dim_coverage = sentinel.src_dim_coverage + src_aux_coverage = sentinel.src_aux_coverage + tgt_dim_coverage = sentinel.tgt_dim_coverage + tgt_aux_coverage = sentinel.tgt_aux_coverage + root = "iris.common.resolve.Resolve" + m_prepare_dim = self.patch(f"{root}._prepare_local_payload_dim") + m_prepare_aux = self.patch(f"{root}._prepare_local_payload_aux") + m_prepare_scalar = self.patch(f"{root}._prepare_local_payload_scalar") + resolve = Resolve() + resolve._prepare_local_payload( + src_dim_coverage, + src_aux_coverage, + tgt_dim_coverage, + tgt_aux_coverage, + ) + self.assertEqual(1, m_prepare_dim.call_count) + expected = [mock.call(src_dim_coverage, tgt_dim_coverage)] + self.assertEqual(expected, m_prepare_dim.call_args_list) + self.assertEqual(1, m_prepare_aux.call_count) + expected = [mock.call(src_aux_coverage, tgt_aux_coverage)] + self.assertEqual(expected, m_prepare_aux.call_args_list) + self.assertEqual(1, m_prepare_scalar.call_count) + expected = [mock.call(src_aux_coverage, tgt_aux_coverage)] + self.assertEqual(expected, m_prepare_scalar.call_args_list) + + +class Test__metadata_prepare(tests.IrisTest): + def setUp(self): + self.src_cube = sentinel.src_cube + self.src_category_local = sentinel.src_category_local + self.src_dim_coverage = sentinel.src_dim_coverage + self.src_aux_coverage = mock.Mock( + common_items_aux=sentinel.src_aux_coverage_common_items_aux, + common_items_scalar=sentinel.src_aux_coverage_common_items_scalar, + ) + self.tgt_cube = sentinel.tgt_cube + self.tgt_category_local = sentinel.tgt_category_local + self.tgt_dim_coverage = sentinel.tgt_dim_coverage + self.tgt_aux_coverage = mock.Mock( + common_items_aux=sentinel.tgt_aux_coverage_common_items_aux, + common_items_scalar=sentinel.tgt_aux_coverage_common_items_scalar, + ) + self.resolve = Resolve() + root = "iris.common.resolve.Resolve" + self.m_prepare_common_dim_payload = self.patch( + f"{root}._prepare_common_dim_payload" + ) + self.m_prepare_common_aux_payload = self.patch( + f"{root}._prepare_common_aux_payload" + ) + self.m_prepare_local_payload = self.patch( + f"{root}._prepare_local_payload" + ) + self.m_prepare_factory_payload = self.patch( + f"{root}._prepare_factory_payload" + ) + + def _check(self): + self.assertIsNone(self.resolve.prepared_category) + self.assertIsNone(self.resolve.prepared_factories) + self.resolve._metadata_prepare() + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, self.resolve.prepared_category) + self.assertEqual([], self.resolve.prepared_factories) + self.assertEqual(1, self.m_prepare_common_dim_payload.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual( + expected, self.m_prepare_common_dim_payload.call_args_list + ) + self.assertEqual(2, self.m_prepare_common_aux_payload.call_count) + expected = [ + mock.call( + self.src_aux_coverage.common_items_aux, + self.tgt_aux_coverage.common_items_aux, + [], + ), + mock.call( + self.src_aux_coverage.common_items_scalar, + self.tgt_aux_coverage.common_items_scalar, + [], + ignore_mismatch=True, + ), + ] + self.assertEqual( + expected, self.m_prepare_common_aux_payload.call_args_list + ) + self.assertEqual(1, self.m_prepare_local_payload.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.src_aux_coverage, + self.tgt_dim_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_prepare_local_payload.call_args_list) + self.assertEqual(2, self.m_prepare_factory_payload.call_count) + expected = [ + mock.call(self.tgt_cube, self.tgt_category_local, from_src=False), + mock.call(self.src_cube, self.src_category_local), + ] + self.assertEqual( + expected, self.m_prepare_factory_payload.call_args_list + ) + + def test_map_rhs_to_lhs__true(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.src_cube + self.resolve.rhs_cube_category_local = self.src_category_local + self.resolve.rhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.lhs_cube = self.tgt_cube + self.resolve.lhs_cube_category_local = self.tgt_category_local + self.resolve.lhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.tgt_aux_coverage + self._check() + + def test_map_rhs_to_lhs__false(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube = self.src_cube + self.resolve.lhs_cube_category_local = self.src_category_local + self.resolve.lhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.rhs_cube = self.tgt_cube + self.resolve.rhs_cube_category_local = self.tgt_category_local + self.resolve.rhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.tgt_aux_coverage + self._check() + + +class Test__prepare_factory_payload(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["aux_factories"]) + self.Coord = namedtuple("Coord", ["metadata"]) + self.Factory_T1 = namedtuple( + "Factory_T1", ["dependencies"] + ) # dummy factory type + self.container_T1 = type(self.Factory_T1(None)) + self.Factory_T2 = namedtuple( + "Factory_T2", ["dependencies"] + ) # dummy factory type + self.container_T2 = type(self.Factory_T2(None)) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.prepared_factories = [] + self.m_get_prepared_item = self.patch( + "iris.common.resolve.Resolve._get_prepared_item" + ) + self.category_local = sentinel.category_local + self.from_src = sentinel.from_src + + def test_no_factory(self): + cube = self.Cube(aux_factories=[]) + self.resolve._prepare_factory_payload(cube, self.category_local) + self.assertEqual(0, len(self.resolve.prepared_factories)) + + def test_skip_factory__already_prepared(self): + aux_factory = self.Factory_T1(dependencies=None) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + prepared_factories = [ + _PreparedFactory(container=self.container_T1, dependencies=None), + _PreparedFactory(container=self.container_T2, dependencies=None), + ] + self.resolve.prepared_factories.extend(prepared_factories) + self.resolve._prepare_factory_payload(cube, self.category_local) + self.assertEqual(prepared_factories, self.resolve.prepared_factories) + + def test_factory__dependency_already_prepared(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (coord_a, coord_b, coord_c) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(1, len(self.resolve.prepared_factories)) + prepared_dependencies = { + name: coord.metadata for name, coord in dependencies.items() + } + expected = [ + _PreparedFactory( + container=self.container_T1, dependencies=prepared_dependencies + ) + ] + self.assertEqual(expected, self.resolve.prepared_factories) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in expected: + self.assertIn(call, actual) + + def test_factory__dependency_local_not_prepared(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (None, coord_a, None, coord_b, None, coord_c) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(1, len(self.resolve.prepared_factories)) + prepared_dependencies = { + name: coord.metadata for name, coord in dependencies.items() + } + expected = [ + _PreparedFactory( + container=self.container_T1, dependencies=prepared_dependencies + ) + ] + self.assertEqual(expected, self.resolve.prepared_factories) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_a.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_b.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_c.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in expected: + self.assertIn(call, actual) + + def test_factory__dependency_not_found(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (None, None) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(0, len(self.resolve.prepared_factories)) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_a.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_b.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_c.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in actual: + self.assertIn(call, expected) + + +class Test__get_prepared_item(tests.IrisTest): + def setUp(self): + PreparedItem = namedtuple("PreparedItem", ["metadata"]) + self.resolve = Resolve() + self.prepared_dim_metadata_src = sentinel.prepared_dim_metadata_src + self.prepared_dim_metadata_tgt = sentinel.prepared_dim_metadata_tgt + self.prepared_items_dim = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_dim_metadata_src, + tgt=self.prepared_dim_metadata_tgt, + ) + ) + self.prepared_aux_metadata_src = sentinel.prepared_aux_metadata_src + self.prepared_aux_metadata_tgt = sentinel.prepared_aux_metadata_tgt + self.prepared_items_aux = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_aux_metadata_src, + tgt=self.prepared_aux_metadata_tgt, + ) + ) + self.prepared_scalar_metadata_src = ( + sentinel.prepared_scalar_metadata_src + ) + self.prepared_scalar_metadata_tgt = ( + sentinel.prepared_scalar_metadata_tgt + ) + self.prepared_items_scalar = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_scalar_metadata_src, + tgt=self.prepared_scalar_metadata_tgt, + ) + ) + self.resolve.prepared_category = _CategoryItems( + items_dim=[self.prepared_items_dim], + items_aux=[self.prepared_items_aux], + items_scalar=[self.prepared_items_scalar], + ) + self.resolve.mapping = {0: 10} + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item" + ) + self.local_dim_metadata = sentinel.local_dim_metadata + self.local_aux_metadata = sentinel.local_aux_metadata + self.local_scalar_metadata = sentinel.local_scalar_metadata + self.local_coord = sentinel.local_coord + self.local_coord_dims = (0,) + self.local_items_dim = _Item( + metadata=self.local_dim_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.local_items_aux = _Item( + metadata=self.local_aux_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.local_items_scalar = _Item( + metadata=self.local_scalar_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.category_local = _CategoryItems( + items_dim=[self.local_items_dim], + items_aux=[self.local_items_aux], + items_scalar=[self.local_items_scalar], + ) + + def test_missing_prepared_coord__from_src(self): + metadata = sentinel.missing + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertIsNone(result) + + def test_missing_prepared_coord__from_tgt(self): + metadata = sentinel.missing + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertIsNone(result) + + def test_get_prepared_dim_coord__from_src(self): + metadata = self.prepared_dim_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_dim, result) + + def test_get_prepared_dim_coord__from_tgt(self): + metadata = self.prepared_dim_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_dim, result) + + def test_get_prepared_aux_coord__from_src(self): + metadata = self.prepared_aux_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_aux, result) + + def test_get_prepared_aux_coord__from_tgt(self): + metadata = self.prepared_aux_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_aux, result) + + def test_get_prepared_scalar_coord__from_src(self): + metadata = self.prepared_scalar_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_scalar, result) + + def test_get_prepared_scalar_coord__from_tgt(self): + metadata = self.prepared_scalar_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_scalar, result) + + def test_missing_local_coord__from_src(self): + metadata = sentinel.missing + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + self.assertIsNone(result) + + def test_missing_local_coord__from_tgt(self): + metadata = sentinel.missing + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + self.assertIsNone(result) + + def test_get_local_dim_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_dim_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(expected, self.resolve.prepared_category.items_dim[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_dim_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_dim_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(expected, self.resolve.prepared_category.items_dim[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_aux_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_aux_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(expected, self.resolve.prepared_category.items_aux[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_aux_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_aux_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(expected, self.resolve.prepared_category.items_aux[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_scalar_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_scalar_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + self.assertEqual( + expected, self.resolve.prepared_category.items_scalar[1] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_scalar_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_scalar_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + self.assertEqual( + expected, self.resolve.prepared_category.items_scalar[1] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test_cube(tests.IrisTest): + def setUp(self): + self.shape = (2, 3) + self.data = np.zeros(np.multiply(*self.shape), dtype=np.int8).reshape( + self.shape + ) + self.bad_data = np.zeros(np.multiply(*self.shape), dtype=np.int8) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve._broadcast_shape = self.shape + self.cube_metadata = CubeMetadata( + standard_name="air_temperature", + long_name="air temp", + var_name="airT", + units=Unit("K"), + attributes={}, + cell_methods=(), + ) + lhs_cube = Cube(self.data) + lhs_cube.metadata = self.cube_metadata + self.resolve.lhs_cube = lhs_cube + rhs_cube = Cube(self.data) + rhs_cube.metadata = self.cube_metadata + self.resolve.rhs_cube = rhs_cube + self.m_add_dim_coord = self.patch("iris.cube.Cube.add_dim_coord") + self.m_add_aux_coord = self.patch("iris.cube.Cube.add_aux_coord") + self.m_add_aux_factory = self.patch("iris.cube.Cube.add_aux_factory") + self.m_coord = self.patch("iris.cube.Cube.coord") + # + # prepared coordinates + # + prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # prepared dim coordinates + self.prepared_dim_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_dim_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_dim_0_points = sentinel.prepared_dim_0_points + self.prepared_dim_0_bounds = sentinel.prepared_dim_0_bounds + self.prepared_dim_0_dims = (0,) + self.prepared_dim_0_coord = mock.Mock(metadata=None) + self.prepared_dim_0_container = mock.Mock( + return_value=self.prepared_dim_0_coord + ) + self.prepared_dim_0 = _PreparedItem( + metadata=self.prepared_dim_0_metadata, + points=self.prepared_dim_0_points, + bounds=self.prepared_dim_0_bounds, + dims=self.prepared_dim_0_dims, + container=self.prepared_dim_0_container, + ) + prepared_category.items_dim.append(self.prepared_dim_0) + self.prepared_dim_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_dim_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_dim_1_points = sentinel.prepared_dim_1_points + self.prepared_dim_1_bounds = sentinel.prepared_dim_1_bounds + self.prepared_dim_1_dims = (1,) + self.prepared_dim_1_coord = mock.Mock(metadata=None) + self.prepared_dim_1_container = mock.Mock( + return_value=self.prepared_dim_1_coord + ) + self.prepared_dim_1 = _PreparedItem( + metadata=self.prepared_dim_1_metadata, + points=self.prepared_dim_1_points, + bounds=self.prepared_dim_1_bounds, + dims=self.prepared_dim_1_dims, + container=self.prepared_dim_1_container, + ) + prepared_category.items_dim.append(self.prepared_dim_1) + + # prepared auxiliary coordinates + self.prepared_aux_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_aux_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_aux_0_points = sentinel.prepared_aux_0_points + self.prepared_aux_0_bounds = sentinel.prepared_aux_0_bounds + self.prepared_aux_0_dims = (0,) + self.prepared_aux_0_coord = mock.Mock(metadata=None) + self.prepared_aux_0_container = mock.Mock( + return_value=self.prepared_aux_0_coord + ) + self.prepared_aux_0 = _PreparedItem( + metadata=self.prepared_aux_0_metadata, + points=self.prepared_aux_0_points, + bounds=self.prepared_aux_0_bounds, + dims=self.prepared_aux_0_dims, + container=self.prepared_aux_0_container, + ) + prepared_category.items_aux.append(self.prepared_aux_0) + self.prepared_aux_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_aux_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_aux_1_points = sentinel.prepared_aux_1_points + self.prepared_aux_1_bounds = sentinel.prepared_aux_1_bounds + self.prepared_aux_1_dims = (1,) + self.prepared_aux_1_coord = mock.Mock(metadata=None) + self.prepared_aux_1_container = mock.Mock( + return_value=self.prepared_aux_1_coord + ) + self.prepared_aux_1 = _PreparedItem( + metadata=self.prepared_aux_1_metadata, + points=self.prepared_aux_1_points, + bounds=self.prepared_aux_1_bounds, + dims=self.prepared_aux_1_dims, + container=self.prepared_aux_1_container, + ) + prepared_category.items_aux.append(self.prepared_aux_1) + + # prepare scalar coordinates + self.prepared_scalar_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_scalar_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_scalar_0_points = sentinel.prepared_scalar_0_points + self.prepared_scalar_0_bounds = sentinel.prepared_scalar_0_bounds + self.prepared_scalar_0_dims = () + self.prepared_scalar_0_coord = mock.Mock(metadata=None) + self.prepared_scalar_0_container = mock.Mock( + return_value=self.prepared_scalar_0_coord + ) + self.prepared_scalar_0 = _PreparedItem( + metadata=self.prepared_scalar_0_metadata, + points=self.prepared_scalar_0_points, + bounds=self.prepared_scalar_0_bounds, + dims=self.prepared_scalar_0_dims, + container=self.prepared_scalar_0_container, + ) + prepared_category.items_scalar.append(self.prepared_scalar_0) + self.prepared_scalar_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_scalar_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_scalar_1_points = sentinel.prepared_scalar_1_points + self.prepared_scalar_1_bounds = sentinel.prepared_scalar_1_bounds + self.prepared_scalar_1_dims = () + self.prepared_scalar_1_coord = mock.Mock(metadata=None) + self.prepared_scalar_1_container = mock.Mock( + return_value=self.prepared_scalar_1_coord + ) + self.prepared_scalar_1 = _PreparedItem( + metadata=self.prepared_scalar_1_metadata, + points=self.prepared_scalar_1_points, + bounds=self.prepared_scalar_1_bounds, + dims=self.prepared_scalar_1_dims, + container=self.prepared_scalar_1_container, + ) + prepared_category.items_scalar.append(self.prepared_scalar_1) + # + # prepared factories + # + prepared_factories = [] + self.aux_factory = sentinel.aux_factory + self.prepared_factory_container = mock.Mock( + return_value=self.aux_factory + ) + self.prepared_factory_metadata_a = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_a_combined, + src=None, + tgt=None, + ) + self.prepared_factory_metadata_b = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_b_combined, + src=None, + tgt=None, + ) + self.prepared_factory_metadata_c = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_c_combined, + src=None, + tgt=None, + ) + self.prepared_factory_dependencies = dict( + name_a=self.prepared_factory_metadata_a, + name_b=self.prepared_factory_metadata_b, + name_c=self.prepared_factory_metadata_c, + ) + self.prepared_factory = _PreparedFactory( + container=self.prepared_factory_container, + dependencies=self.prepared_factory_dependencies, + ) + prepared_factories.append(self.prepared_factory) + self.prepared_factory_side_effect = ( + sentinel.prepared_factory_coord_a, + sentinel.prepared_factory_coord_b, + sentinel.prepared_factory_coord_c, + ) + self.m_coord.side_effect = self.prepared_factory_side_effect + self.resolve.prepared_category = prepared_category + self.resolve.prepared_factories = prepared_factories + + def test_no_resolved_shape(self): + self.resolve._broadcast_shape = None + data = None + emsg = "Cannot resolve resultant cube, as no candidate cubes have been provided" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(data) + + def test_bad_data_shape(self): + emsg = "Cannot resolve resultant cube, as the provided data must have shape" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(self.bad_data) + + def test_bad_data_shape__inplace(self): + self.resolve.lhs_cube = Cube(self.bad_data) + emsg = "Cannot resolve resultant cube in-place" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(self.data, in_place=True) + + def _check(self): + # check dim coordinate 0 + self.assertEqual(1, self.prepared_dim_0.container.call_count) + expected = [ + mock.call( + self.prepared_dim_0_points, bounds=self.prepared_dim_0_bounds + ) + ] + self.assertEqual( + expected, self.prepared_dim_0.container.call_args_list + ) + self.assertEqual( + self.prepared_dim_0_coord.metadata, + self.prepared_dim_0_metadata.combined, + ) + # check dim coordinate 1 + self.assertEqual(1, self.prepared_dim_1.container.call_count) + expected = [ + mock.call( + self.prepared_dim_1_points, bounds=self.prepared_dim_1_bounds + ) + ] + self.assertEqual( + expected, self.prepared_dim_1.container.call_args_list + ) + self.assertEqual( + self.prepared_dim_1_coord.metadata, + self.prepared_dim_1_metadata.combined, + ) + # check add_dim_coord + self.assertEqual(2, self.m_add_dim_coord.call_count) + expected = [ + mock.call(self.prepared_dim_0_coord, self.prepared_dim_0_dims), + mock.call(self.prepared_dim_1_coord, self.prepared_dim_1_dims), + ] + self.assertEqual(expected, self.m_add_dim_coord.call_args_list) + + # check aux coordinate 0 + self.assertEqual(1, self.prepared_aux_0.container.call_count) + expected = [ + mock.call( + self.prepared_aux_0_points, bounds=self.prepared_aux_0_bounds + ) + ] + self.assertEqual( + expected, self.prepared_aux_0.container.call_args_list + ) + self.assertEqual( + self.prepared_aux_0_coord.metadata, + self.prepared_aux_0_metadata.combined, + ) + # check aux coordinate 1 + self.assertEqual(1, self.prepared_aux_1.container.call_count) + expected = [ + mock.call( + self.prepared_aux_1_points, bounds=self.prepared_aux_1_bounds + ) + ] + self.assertEqual( + expected, self.prepared_aux_1.container.call_args_list + ) + self.assertEqual( + self.prepared_aux_1_coord.metadata, + self.prepared_aux_1_metadata.combined, + ) + # check scalar coordinate 0 + self.assertEqual(1, self.prepared_scalar_0.container.call_count) + expected = [ + mock.call( + self.prepared_scalar_0_points, + bounds=self.prepared_scalar_0_bounds, + ) + ] + self.assertEqual( + expected, self.prepared_scalar_0.container.call_args_list + ) + self.assertEqual( + self.prepared_scalar_0_coord.metadata, + self.prepared_scalar_0_metadata.combined, + ) + # check scalar coordinate 1 + self.assertEqual(1, self.prepared_scalar_1.container.call_count) + expected = [ + mock.call( + self.prepared_scalar_1_points, + bounds=self.prepared_scalar_1_bounds, + ) + ] + self.assertEqual( + expected, self.prepared_scalar_1.container.call_args_list + ) + self.assertEqual( + self.prepared_scalar_1_coord.metadata, + self.prepared_scalar_1_metadata.combined, + ) + # check add_aux_coord + self.assertEqual(4, self.m_add_aux_coord.call_count) + expected = [ + mock.call(self.prepared_aux_0_coord, self.prepared_aux_0_dims), + mock.call(self.prepared_aux_1_coord, self.prepared_aux_1_dims), + mock.call( + self.prepared_scalar_0_coord, self.prepared_scalar_0_dims + ), + mock.call( + self.prepared_scalar_1_coord, self.prepared_scalar_1_dims + ), + ] + self.assertEqual(expected, self.m_add_aux_coord.call_args_list) + + # check auxiliary factories + self.assertEqual(1, self.m_add_aux_factory.call_count) + expected = [mock.call(self.aux_factory)] + self.assertEqual(expected, self.m_add_aux_factory.call_args_list) + self.assertEqual(1, self.prepared_factory_container.call_count) + expected = [ + mock.call( + **{ + name: value + for name, value in zip( + sorted(self.prepared_factory_dependencies.keys()), + self.prepared_factory_side_effect, + ) + } + ) + ] + self.assertEqual( + expected, self.prepared_factory_container.call_args_list + ) + self.assertEqual(3, self.m_coord.call_count) + expected = [ + mock.call(self.prepared_factory_metadata_a.combined), + mock.call(self.prepared_factory_metadata_b.combined), + mock.call(self.prepared_factory_metadata_c.combined), + ] + self.assertEqual(expected, self.m_coord.call_args_list) + + def test_resolve(self): + result = self.resolve.cube(self.data) + self.assertEqual(self.cube_metadata, result.metadata) + self._check() + self.assertIsNot(self.resolve.lhs_cube, result) + + def test_resolve__inplace(self): + result = self.resolve.cube(self.data, in_place=True) + self.assertEqual(self.cube_metadata, result.metadata) + self._check() + self.assertIs(self.resolve.lhs_cube, result) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/cube/test_Cube.py b/lib/iris/tests/unit/cube/test_Cube.py index 63553ac821c..ded401cab31 100644 --- a/lib/iris/tests/unit/cube/test_Cube.py +++ b/lib/iris/tests/unit/cube/test_Cube.py @@ -336,6 +336,108 @@ def test_non_lazy_aggregator(self): self.assertArrayEqual(result.data, np.mean(self.data, axis=1)) +class Test_collapsed__multidim_weighted(tests.IrisTest): + def setUp(self): + self.data = np.arange(6.0).reshape((2, 3)) + self.lazydata = as_lazy_data(self.data) + # Test cubes wth (same-valued) real and lazy data + cube_real = Cube(self.data) + for i_dim, name in enumerate(("y", "x")): + npts = cube_real.shape[i_dim] + coord = DimCoord(np.arange(npts), long_name=name) + cube_real.add_dim_coord(coord, i_dim) + self.cube_real = cube_real + self.cube_lazy = cube_real.copy(data=self.lazydata) + # Test weights and expected result for a y-collapse + self.y_weights = np.array([0.3, 0.5]) + self.full_weights_y = np.broadcast_to( + self.y_weights.reshape((2, 1)), cube_real.shape + ) + self.expected_result_y = np.array([1.875, 2.875, 3.875]) + # Test weights and expected result for an x-collapse + self.x_weights = np.array([0.7, 0.4, 0.6]) + self.full_weights_x = np.broadcast_to( + self.x_weights.reshape((1, 3)), cube_real.shape + ) + self.expected_result_x = np.array([0.941176, 3.941176]) + + def test_weighted_fullweights_real_y(self): + # Supplying full-shape weights for collapsing over a single dimension. + cube_collapsed = self.cube_real.collapsed( + "y", MEAN, weights=self.full_weights_y + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_fullweights_lazy_y(self): + # Full-shape weights, lazy data : Check lazy result, same values as real calc. + cube_collapsed = self.cube_lazy.collapsed( + "y", MEAN, weights=self.full_weights_y + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_1dweights_real_y(self): + # 1-D weights, real data : Check same results as full-shape. + cube_collapsed = self.cube_real.collapsed( + "y", MEAN, weights=self.y_weights + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_1dweights_lazy_y(self): + # 1-D weights, lazy data : Check lazy result, same values as real calc. + cube_collapsed = self.cube_lazy.collapsed( + "y", MEAN, weights=self.y_weights + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_y + ) + + def test_weighted_fullweights_real_x(self): + # Full weights, real data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_real.collapsed( + "x", MEAN, weights=self.full_weights_x + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + def test_weighted_fullweights_lazy_x(self): + # Full weights, lazy data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_lazy.collapsed( + "x", MEAN, weights=self.full_weights_x + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + def test_weighted_1dweights_real_x(self): + # 1-D weights, real data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_real.collapsed( + "x", MEAN, weights=self.x_weights + ) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + def test_weighted_1dweights_lazy_x(self): + # 1-D weights, lazy data, ** collapse X ** : as for 'y' case above + cube_collapsed = self.cube_lazy.collapsed( + "x", MEAN, weights=self.x_weights + ) + self.assertTrue(cube_collapsed.has_lazy_data()) + self.assertArrayAlmostEqual( + cube_collapsed.data, self.expected_result_x + ) + + class Test_collapsed__cellmeasure_ancils(tests.IrisTest): def setUp(self): cube = Cube(np.arange(6.0).reshape((2, 3))) @@ -484,6 +586,16 @@ def test_ancillary_variable(self): ) self.assertEqual(cube.summary(), expected_summary) + def test_similar_coords(self): + coord1 = AuxCoord( + 42, long_name="foo", attributes=dict(bar=np.array([2, 5])) + ) + coord2 = coord1.copy() + coord2.attributes = dict(bar="baz") + for coord in [coord1, coord2]: + self.cube.add_aux_coord(coord) + self.assertIn("baz", self.cube.summary()) + class Test_is_compatible(tests.IrisTest): def setUp(self): diff --git a/noxfile.py b/noxfile.py index cd97e8ef8b6..7bfcc73dd74 100644 --- a/noxfile.py +++ b/noxfile.py @@ -19,7 +19,7 @@ PACKAGE = str("lib" / Path("iris")) #: Cirrus-CI environment variable hook. -PY_VER = os.environ.get("PY_VER", "3.7") +PY_VER = os.environ.get("PY_VER", ["3.6", "3.7"]) #: Default cartopy cache directory. CARTOPY_CACHE_DIR = os.environ.get("HOME") / Path(".local/share/cartopy") @@ -41,7 +41,7 @@ def venv_cached(session): """ result = False - yml = Path(f"requirements/ci/py{PY_VER.replace('.', '')}.yml") + yml = Path(f"requirements/ci/py{session.python.replace('.', '')}.yml") tmp_dir = Path(session.create_tmp()) cache = tmp_dir / yml.name if cache.is_file(): @@ -66,7 +66,7 @@ def cache_venv(session): A `nox.sessions.Session` object. """ - yml = Path(f"requirements/ci/py{PY_VER.replace('.', '')}.yml") + yml = Path(f"requirements/ci/py{session.python.replace('.', '')}.yml") with open(yml, "rb") as fi: hexdigest = hashlib.sha256(fi.read()).hexdigest() tmp_dir = Path(session.create_tmp()) @@ -131,7 +131,7 @@ def black(session): session.run("black", "--check", __file__) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def tests(session): """ Perform iris system, integration and unit tests. @@ -150,7 +150,7 @@ def tests(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -164,7 +164,7 @@ def tests(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.run( "python", "-m", @@ -174,7 +174,7 @@ def tests(session): ) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def gallery(session): """ Perform iris gallery doc-tests. @@ -193,7 +193,7 @@ def gallery(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -207,7 +207,7 @@ def gallery(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.run( "python", "-m", @@ -216,7 +216,7 @@ def gallery(session): ) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def doctest(session): """ Perform iris doc-tests. @@ -235,7 +235,7 @@ def doctest(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -249,7 +249,7 @@ def doctest(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.cd("docs/iris") session.run( "make", @@ -264,7 +264,7 @@ def doctest(session): ) -@nox.session(python=[PY_VER], venv_backend="conda") +@nox.session(python=PY_VER, venv_backend="conda") def linkcheck(session): """ Perform iris doc link check. @@ -283,7 +283,7 @@ def linkcheck(session): """ if not venv_cached(session): # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{PY_VER.replace('.', '')}.yml" + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" # Back-door approach to force nox to use "conda env update". command = ( "conda", @@ -297,7 +297,7 @@ def linkcheck(session): cache_venv(session) cache_cartopy(session) - session.run("python", "setup.py", "develop") + session.install("--no-deps", "--editable", ".") session.cd("docs/iris") session.run( "make", diff --git a/requirements/ci/py36.yml b/requirements/ci/py36.yml index 2b40fbad4e5..4d9d25d7c61 100644 --- a/requirements/ci/py36.yml +++ b/requirements/ci/py36.yml @@ -44,11 +44,8 @@ dependencies: # Documentation dependencies. - sphinx + - sphinxcontrib-napoleon - sphinx-copybutton - sphinx-gallery + - sphinx-panels - sphinx_rtd_theme - - pip - - pip: - - sphinxcontrib-napoleon - - sphinx-panels - diff --git a/requirements/ci/py37.yml b/requirements/ci/py37.yml index 0f01f0ef755..bdb097796a4 100644 --- a/requirements/ci/py37.yml +++ b/requirements/ci/py37.yml @@ -44,10 +44,8 @@ dependencies: # Documentation dependencies. - sphinx + - sphinxcontrib-napoleon - sphinx-copybutton - sphinx-gallery + - sphinx-panels - sphinx_rtd_theme - - pip - - pip: - - sphinxcontrib-napoleon - - sphinx-panels From f0d72f72b66bae6ae9aca668cd7bc7060bb8cd7e Mon Sep 17 00:00:00 2001 From: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Date: Wed, 27 Jan 2021 12:22:13 +0000 Subject: [PATCH 09/23] Captilise installation heading - align #3958 content with #3940. (#3963) --- docs/iris/src/installing.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/iris/src/installing.rst b/docs/iris/src/installing.rst index cd6a648516e..8b3ae8d3e7c 100644 --- a/docs/iris/src/installing.rst +++ b/docs/iris/src/installing.rst @@ -43,8 +43,8 @@ at https://conda.io/en/latest/index.html. .. _installing_from_source_without_conda: -Installing from source without conda on Debian-based Linux distros (devs) -------------------------------------------------------------------------- +Installing from Source Without Conda on Debian-Based Linux Distros (Developers) +------------------------------------------------------------------------------- Iris can also be installed without a conda environment. The instructions in this section are valid for Debian-based Linux distributions (Debian, Ubuntu, @@ -55,11 +55,11 @@ These can be installed with apt:: sudo apt-get install python3-pip python3-tk libudunits2-dev libproj-dev proj-bin libgeos-dev libcunit1-dev - + Consider executing:: sudo apt-get update - + before and after installation of Debian packages. The rest can be done with pip. Begin with numpy:: From 40def237ecd23e1175e1586c12ed434221f99120 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Thu, 28 Jan 2021 11:09:52 +0000 Subject: [PATCH 10/23] Merge back v3p0p1 (#3966) * Add release highlights and pin rc version (#3898) * Add release highlights and pin rc version * review actions * reorder release highlights (#3899) Tweak release highlights * Add whatsnew announcement (#3900) * Fix spelling (#3903) * Fix unit label handling (#3902) * Add failing test of plotting * Implement fix to pass test * Update idiff to ignore irrelevant hyphens in path * Update imagerepo (following docs) * Update after review by @trexfeathers * Add whatsnew entries * Move whatsnew entries into correct file * Release Docs Improvements (#3895) * Minor phrasing change in 'Release candidate'. * Before release deprecations. * Whatsnew highlights section. * Relax setup.py setup requirements (#3909) * Updated CF saver version in User Guide and docstring (#3925) * Updated CF saver version in User Guide and docstring * Remove references to CF version of the loader in docstrings * Added whatsnew * Pin cftime<1.3.0 * Migrate to cirrus-ci (#3928) * migrate from travis-ci to cirrus-ci * added whatsnew entries * ignore url for doc link check (#3929) * whatsnew for coord default units (#3924) * Cube._summary_coord_extra: efficiency and bugfix (#3922) * Add Documentation Title Case Capitalization (#3940) * Use Title Case Capitalisation for Documentation * add whatsnew enter * CI requirements drop pip packages (#3939) * requirements pip to conda * use pip install over develop * default PY_VER to python versions * update links (#3942) * update links * added s to http * Add support for 1-d weights in collapse. (#3943) * Remove warning for convert_units on lazy data (#3951) * drop stickler references in docs (#3953) * drop stickler references in docs * remove sticker from common links * update docs for travis-ci to cirrus-ci (#3954) * update docs for travis-ci to cirrus-ci * add 'travis-ci' reference locally to whatsnew * update whatsnew comment * docs for nox (#3955) * docs for nox * add titles, notices and additional detail * review actions * Resolve test coverage (#3947) * test coverage for __init__ and __call__ * test coverage for metadata resolve and coverage * partial test coverage for metadata mapping * python 3.6 workaround for deepcopy of mock.sentinel * test coverage for Resolve._free_mapping * test coverage for Resolve convenience methods * add test stub for Resolve._metadata_mapping * fix Test__tgt_cube_position * test coverage for shape * test coverage for _as_compatible_cubes * test coverage for Resolve._metadata_mapping * test coverage for Resolve._prepare_common_dim_payload * test coverage for Resolve._prepare_common_aux_payload * test coverage for Resolve._prepare_points_and_bounds * test coverage for Resolve._create_prepared_item * test coverage for Resolve._prepare_local_payload_dim * test coverage for Resolve._prepare_local_payload_aux * test coverage for Resolve._prepare_local_payload_scalar + docs URL skip * test coverage for Resolve._prepare_local_payload * test coverage for Resolve._metadata_prepare * added docs URL linkcheck skip * test coverage for Resolve._prepare_factory_payload * test coverage for Resolve._get_prepared_item * review actions * test coverage for Resolve.cube * pin v3.0.0 version and whatnew date (#3956) * update github ci checks image (#3957) * Promote unknown units to dimensionless in aux factories (#3965) * promote unknown to dimensionless units in aux factories * patch aux factories to promote unknown to dimensionless units for formula terms * add whatnew PR for entry Co-authored-by: tkknight <2108488+tkknight@users.noreply.github.com> Co-authored-by: Zeb Nicholls Co-authored-by: Martin Yeo <40734014+trexfeathers@users.noreply.github.com> Co-authored-by: Jon Seddon <17068361+jonseddon@users.noreply.github.com> Co-authored-by: Ruth Comer Co-authored-by: Patrick Peglar --- docs/iris/src/conf.py | 4 +- docs/iris/src/whatsnew/3.0.1.rst | 520 ++++++++++++++++++ docs/iris/src/whatsnew/index.rst | 1 + lib/iris/aux_factory.py | 25 + lib/iris/fileformats/netcdf.py | 3 + .../aux_factory/test_HybridPressureFactory.py | 9 + .../unit/aux_factory/test_OceanSFactory.py | 6 + .../unit/aux_factory/test_OceanSg1Factory.py | 9 + .../unit/aux_factory/test_OceanSg2Factory.py | 9 + .../aux_factory/test_OceanSigmaFactory.py | 6 + .../aux_factory/test_OceanSigmaZFactory.py | 6 + .../netcdf/test__load_aux_factory.py | 41 +- 12 files changed, 635 insertions(+), 4 deletions(-) create mode 100644 docs/iris/src/whatsnew/3.0.1.rst diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index 7232d7c40ef..8be849b7ceb 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -82,8 +82,8 @@ def autolog(message): if iris.__version__ == "dev": version = "dev" else: - # major.feature(.minor)-dev -> major.minor - version = ".".join(iris.__version__.split("-")[0].split(".")[:2]) + # major.minor.patch-dev -> major.minor.patch + version = ".".join(iris.__version__.split("-")[0].split(".")[:3]) # The full version, including alpha/beta/rc tags. release = iris.__version__ diff --git a/docs/iris/src/whatsnew/3.0.1.rst b/docs/iris/src/whatsnew/3.0.1.rst new file mode 100644 index 00000000000..597e235ebea --- /dev/null +++ b/docs/iris/src/whatsnew/3.0.1.rst @@ -0,0 +1,520 @@ +.. include:: ../common_links.inc + +v3.0.1 (27 Jan 2021) +******************** + +This document explains the changes made to Iris for this release +(:doc:`View all changes `.) + + +.. dropdown:: :opticon:`alert` v3.0.1 Patches + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The patches included in this release include: + + 💼 **Internal** + + * `@bjlittle`_ gracefully promote formula terms within :mod:`~iris.aux_factory` that have ``units`` of ``unknown`` + to ``units`` of ``1`` (dimensionless), where the formula term **must** have dimensionless ``units``. Without this + graceful treatment of ``units`` the resulting :class:`~iris.cube.Cube` will **not** contain the expected auxiliary + factory, and the associated derived coordinate will be missing. (:pull:`3965`) + + +.. dropdown:: :opticon:`report` Release Highlights + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The highlights for this major release of Iris include: + + * We've finally dropped support for ``Python 2``, so welcome to ``Iris 3`` + and ``Python 3``! + * We've extended our coverage of the `CF Conventions and Metadata`_ by + introducing support for `CF Ancillary Data`_ and `Quality Flags`_, + * Lazy regridding is now available for several regridding schemes, + * Managing and manipulating metadata within Iris is now easier and more + consistent thanks to the introduction of a new common metadata API, + * :ref:`Cube arithmetic ` has been significantly improved with + regards to extended broadcasting, auto-transposition and a more lenient + behaviour towards handling metadata and coordinates, + * Our :ref:`documentation ` has been refreshed, + restructured, revitalised and rehosted on `readthedocs`_, + * It's now easier than ever to :ref:`install Iris ` + as a user or a developer, and the newly revamped developers guide walks + you though how you can :ref:`get involved ` + and contribute to Iris, + * Also, this is a major release of Iris, so please be aware of the + :ref:`incompatible changes ` and + :ref:`deprecations `. + + And finally, get in touch with us on `GitHub`_ if you have any issues or + feature requests for improving Iris. Enjoy! + + +📢 Announcements +================ + +* Congratulations to `@bouweandela`_, `@jvegasbsc`_, and `@zklaus`_ who + recently became Iris core developers. They bring a wealth of expertise to the + team, and are using Iris to underpin `ESMValTool`_ - "*A community diagnostic + and performance metrics tool for routine evaluation of Earth system models + in CMIP*". Welcome aboard! 🎉 + +* Congratulations also goes to `@jonseddon`_ who recently became an Iris core + developer. We look forward to seeing more of your awesome contributions! 🎉 + + +✨ Features +=========== + +* `@MoseleyS`_ greatly enhanced the :mod:`~iris.fileformats.nimrod` + module to provide richer meta-data translation when loading ``Nimrod`` data + into cubes. This covers most known operational use-cases. (:pull:`3647`) + +* `@stephenworsley`_ improved the handling of + :class:`iris.coords.CellMeasure`\ s in the :class:`~iris.cube.Cube` + statistical operations :meth:`~iris.cube.Cube.collapsed`, + :meth:`~iris.cube.Cube.aggregated_by` and + :meth:`~iris.cube.Cube.rolling_window`. These previously removed every + :class:`~iris.coords.CellMeasure` attached to the cube. Now, a + :class:`~iris.coords.CellMeasure` will only be removed if it is associated + with an axis over which the statistic is being run. (:pull:`3549`) + +* `@stephenworsley`_, `@pp-mo`_ and `@abooton`_ added support for + `CF Ancillary Data`_ variables. These are created as + :class:`iris.coords.AncillaryVariable`, and appear as components of cubes + much like :class:`~iris.coords.AuxCoord`\ s, with the new + :class:`~iris.cube.Cube` methods + :meth:`~iris.cube.Cube.add_ancillary_variable`, + :meth:`~iris.cube.Cube.remove_ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variables` and + :meth:`~iris.cube.Cube.ancillary_variable_dims`. + They are loaded from and saved to NetCDF-CF files. Special support for + `Quality Flags`_ is also provided, to ensure they load and save with + appropriate units. (:pull:`3800`) + +* `@bouweandela`_ implemented lazy regridding for the + :class:`~iris.analysis.Linear`, :class:`~iris.analysis.Nearest`, and + :class:`~iris.analysis.AreaWeighted` regridding schemes. (:pull:`3701`) + +* `@bjlittle`_ added `logging`_ support within :mod:`iris.analysis.maths`, + :mod:`iris.common.metadata`, and :mod:`iris.common.resolve`. Each module + defines a :class:`logging.Logger` instance called ``logger`` with a default + ``level`` of ``INFO``. To enable ``DEBUG`` logging use + ``logger.setLevel("DEBUG")``. (:pull:`3785`) + +* `@bjlittle`_ added the :mod:`iris.common.resolve` module, which provides + infrastructure to support the analysis, identification and combination + of metadata common between two :class:`~iris.cube.Cube` operands into a + single resultant :class:`~iris.cube.Cube` that will be auto-transposed, + and with the appropriate broadcast shape. (:pull:`3785`) + +* `@bjlittle`_ added the :ref:`common metadata API `, which provides + a unified treatment of metadata across Iris, and allows users to easily + manage and manipulate their metadata in a consistent way. (:pull:`3785`) + +* `@bjlittle`_ added :ref:`lenient metadata ` support, to + allow users to control **strict** or **lenient** metadata equivalence, + difference and combination. (:pull:`3785`) + +* `@bjlittle`_ added :ref:`lenient cube maths ` support and + resolved several long standing major issues with cube arithmetic regarding + a more robust treatment of cube broadcasting, cube dimension auto-transposition, + and preservation of common metadata and coordinates during cube math operations. + Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) + +* `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when + collapsing over a single dimension. + Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. + The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works + with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) + + +🐛 Bugs Fixed +============= + +* `@stephenworsley`_ fixed :meth:`~iris.cube.Cube.remove_coord` to now also + remove derived coordinates by removing aux_factories. (:pull:`3641`) + +* `@jonseddon`_ fixed ``isinstance(cube, collections.Iterable)`` to now behave + as expected if a :class:`~iris.cube.Cube` is iterated over, while also + ensuring that ``TypeError`` is still raised. (Fixed by setting the + ``__iter__()`` method in :class:`~iris.cube.Cube` to ``None``). + (:pull:`3656`) + +* `@stephenworsley`_ enabled cube concatenation along an axis shared by cell + measures; these cell measures are now concatenated together in the resulting + cube. Such a scenario would previously cause concatenation to inappropriately + fail. (:pull:`3566`) + +* `@stephenworsley`_ newly included :class:`~iris.coords.CellMeasure`\ s in + :class:`~iris.cube.Cube` copy operations. Previously copying a + :class:`~iris.cube.Cube` would ignore any attached + :class:`~iris.coords.CellMeasure`. (:pull:`3546`) + +* `@bjlittle`_ set a :class:`~iris.coords.CellMeasure`'s + ``measure`` attribute to have a default value of ``area``. + Previously, the ``measure`` was provided as a keyword argument to + :class:`~iris.coords.CellMeasure` with a default value of ``None``, which + caused a ``TypeError`` when no ``measure`` was provided, since ``area`` or + ``volume`` are the only accepted values. (:pull:`3533`) + +* `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use + `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot + axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` + did not include this behaviour). (:pull:`3762`) + +* `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to + now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ + (previously would take the unit from a time coordinate, if present, even + though the coordinate's value had been changed via ``date2num``). + (:pull:`3762`) + +* `@pp-mo`_ newly included attributes of cell measures in NETCDF-CF + file loading; they were previously being discarded. They are now available on + the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. + (:pull:`3800`) + +* `@pp-mo`_ fixed the netcdf loader to now handle any grid-mapping + variables with missing ``false_easting`` and ``false_northing`` properties, + which was previously failing for some coordinate systems. See :issue:`3629`. + (:pull:`3804`) + +* `@stephenworsley`_ changed the way tick labels are assigned from string coords. + Previously, the first tick label would occasionally be duplicated. This also + removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) + +* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check + ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) + +* `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's + coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) + +.. _whatsnew 3.0.1 changes: + +💣 Incompatible Changes +======================= + +* `@pp-mo`_ rationalised :class:`~iris.cube.CubeList` extraction + methods: + + The former method ``iris.cube.CubeList.extract_strict``, and the ``strict`` + keyword of the :meth:`~iris.cube.CubeList.extract` method have been removed, + and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` + and :meth:`~iris.cube.CubeList.extract_cubes`. + The new routines perform the same operation, but in a style more like other + ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. + Unlike ``strict`` extraction, the type of return value is now completely + consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a + :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` + always returns an :class:`iris.cube.CubeList` of a length equal to the + number of constraints. (:pull:`3715`) + +* `@pp-mo`_ removed the former function + ``iris.analysis.coord_comparison``. (:pull:`3562`) + +* `@bjlittle`_ moved the + :func:`iris.experimental.equalise_cubes.equalise_attributes` function from + the :mod:`iris.experimental` module into the :mod:`iris.util` module. Please + use the :func:`iris.util.equalise_attributes` function instead. + (:pull:`3527`) + +* `@bjlittle`_ removed the module ``iris.experimental.concatenate``. In + ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the + :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the + :func:`iris.experimental.concatenate.concatenate` function raised an + exception. (:pull:`3523`) + +* `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` + and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) + +* `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have + ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` + property. Previously these cases defaulted to ``units='1'``. + This affects loading of coordinates whose file variable has no "units" + attribute (not valid, under `CF units rules`_): These will now have units + of `"unknown"`, rather than `"1"`, which **may prevent the creation of + a hybrid vertical coordinate**. While these cases used to "work", this was + never really correct behaviour. (:pull:`3795`) + +* `@SimonPeatman`_ added attribute ``var_name`` to coordinates created by the + :func:`iris.analysis.trajectory.interpolate` function. This prevents + duplicate coordinate errors in certain circumstances. (:pull:`3718`) + +* `@bjlittle`_ aligned the :func:`iris.analysis.maths.apply_ufunc` with the + rest of the :mod:`iris.analysis.maths` API by changing its keyword argument + from ``other_cube`` to ``other``. (:pull:`3785`) + +* `@bjlittle`_ changed the :meth:`iris.analysis.maths.IFunc.__call__` to ignore + any surplus ``other`` keyword argument for a ``data_func`` that requires + **only one** argument. This aligns the behaviour of + :meth:`iris.analysis.maths.IFunc.__call__` with + :func:`~iris.analysis.maths.apply_ufunc`. Previously a ``ValueError`` + exception was raised. (:pull:`3785`) + + +.. _whatsnew 3.0.1 deprecations: + +🔥 Deprecations +=============== + +* `@stephenworsley`_ removed the deprecated :class:`iris.Future` flags + ``cell_date_time_objects``, ``netcdf_promote``, ``netcdf_no_unlimited`` and + ``clip_latitudes``. (:pull:`3459`) + +* `@stephenworsley`_ changed :attr:`iris.fileformats.pp.PPField.lbproc` to be an + ``int``. The deprecated attributes ``flag1``, ``flag2`` etc. have been + removed from it. (:pull:`3461`) + +* `@bjlittle`_ deprecated :func:`~iris.util.as_compatible_shape` in preference + for :class:`~iris.common.resolve.Resolve` e.g., ``Resolve(src, tgt)(tgt.core_data())``. + The :func:`~iris.util.as_compatible_shape` function will be removed in a future + release of Iris. (:pull:`3892`) + + +🔗 Dependencies +=============== + +* `@stephenworsley`_, `@trexfeathers`_ and `@bjlittle`_ removed ``Python2`` + support, modernising the codebase by switching to exclusive ``Python3`` + support. (:pull:`3513`) + +* `@bjlittle`_ improved the developer set up process. Configuring Iris and + :ref:`installing_from_source` as a developer with all the required package + dependencies is now easier with our curated conda environment YAML files. + (:pull:`3812`) + +* `@stephenworsley`_ pinned Iris to require `Dask`_ ``>=2.0``. (:pull:`3460`) + +* `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require + `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version + of `Matplotlib`_. (:pull:`3762`) + +* `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. + Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in + pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer + necessary now that ``Python2`` support has been dropped. (:pull:`3468`) + +* `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version + of `Proj`_. (:pull:`3762`) + +* `@stephenworsley`_ and `@trexfeathers`_ removed GDAL from the extensions + dependency group. We no longer consider it to be an extension. (:pull:`3762`) + + +.. _whatsnew 3.0.1 docs: + +📚 Documentation +================ + +* `@tkknight`_ moved the + :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` + from the general part of the gallery to oceanography. (:pull:`3761`) + +* `@tkknight`_ updated documentation to use a modern sphinx theme and be + served from https://scitools-iris.readthedocs.io/en/latest/. (:pull:`3752`) + +* `@bjlittle`_ added support for the `black`_ code formatter. This is + now automatically checked on GitHub PRs, replacing the older, unittest-based + ``iris.tests.test_coding_standards.TestCodeFormat``. Black provides automatic + code format correction for most IDEs. See the new developer guide section on + :ref:`code_formatting`. (:pull:`3518`) + +* `@tkknight`_ and `@trexfeathers`_ refreshed the :ref:`whats_new_contributions` + for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` + what's new page so it appears on the latest documentation at + https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves + :issue:`2104`, :issue:`3451`, :issue:`3818`, :issue:`3837`. Also updated the + :ref:`iris_development_releases_steps` to follow when making a release. + (:pull:`3769`, :pull:`3838`, :pull:`3843`) + +* `@tkknight`_ enabled the PDF creation of the documentation on the + `Read the Docs`_ service. The PDF may be accessed by clicking on the version + at the bottom of the side bar, then selecting ``PDF`` from the ``Downloads`` + section. (:pull:`3765`) + +* `@stephenworsley`_ added a warning to the + :func:`iris.analysis.cartography.project` function regarding its behaviour on + projections with non-rectangular boundaries. (:pull:`3762`) + +* `@stephenworsley`_ added the :ref:`cube_maths_combining_units` section to the + user guide to clarify how ``Units`` are handled during cube arithmetic. + (:pull:`3803`) + +* `@tkknight`_ overhauled the :ref:`developers_guide` including information on + getting involved in becoming a contributor and general structure of the + guide. This resolves :issue:`2170`, :issue:`2331`, :issue:`3453`, + :issue:`314`, :issue:`2902`. (:pull:`3852`) + +* `@rcomer`_ added argument descriptions to the :class:`~iris.coords.DimCoord` + docstring. (:pull:`3681`) + +* `@tkknight`_ added two url's to be ignored for the ``make linkcheck``. This + will ensure the Iris github project is not repeatedly hit during the + linkcheck for issues and pull requests as it can result in connection + refused and thus travis-ci_ job failures. For more information on linkcheck, + see :ref:`contributing.documentation.testing`. (:pull:`3873`) + +* `@tkknight`_ enabled the napolean_ package that is used by sphinx_ to cater + for the existing google style docstrings and to also allow for `numpy`_ + docstrings. This resolves :issue:`3841`. (:pull:`3871`) + +* `@tkknight`_ configured ``sphinx-build`` to promote warnings to errors when + building the documentation via ``make html``. This will minimise technical + debt accruing for the documentation. (:pull:`3877`) + +* `@tkknight`_ updated :ref:`installing_iris` to include a reference to + Windows Subsystem for Linux. (:pull:`3885`) + +* `@tkknight`_ updated the :ref:`iris_docs` homepage to include panels so the + links are more visible to users. This uses the sphinx-panels_ extension. + (:pull:`3884`) + +* `@bjlittle`_ created the :ref:`Further topics ` section and + included documentation for :ref:`metadata`, :ref:`lenient metadata`, and + :ref:`lenient maths`. (:pull:`3890`) + +* `@jonseddon`_ updated the CF version of the netCDF saver in the + :ref:`saving_iris_cubes` section and in the equivalent function docstring. + (:pull:`3925`) + +* `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. + (:pull:`3940`) + + +💼 Internal +=========== + +* `@pp-mo`_ and `@lbdreyer`_ removed all Iris test dependencies on `iris-grib`_ + by transferring all relevant content to the `iris-grib`_ repository. (:pull:`3662`, + :pull:`3663`, :pull:`3664`, :pull:`3665`, :pull:`3666`, :pull:`3669`, + :pull:`3670`, :pull:`3671`, :pull:`3672`, :pull:`3742`, :pull:`3746`) + +* `@lbdreyer`_ and `@pp-mo`_ overhauled the handling of dimensional + metadata to remove duplication. (:pull:`3422`, :pull:`3551`) + +* `@trexfeathers`_ simplified the standard license header for all files, which + removes the need to repeatedly update year numbers in the header. + (:pull:`3489`) + +* `@stephenworsley`_ changed the numerical values in tests involving the + Robinson projection due to improvements made in + `Proj`_. (:pull:`3762`) (see also `Proj#1292`_ and `Proj#2151`_) + +* `@stephenworsley`_ changed tests to account for more detailed descriptions of + projections in `GDAL`_. (:pull:`3762`) (see also `GDAL#1185`_) + +* `@stephenworsley`_ changed tests to account for `GDAL`_ now saving fill values + for data without masked points. (:pull:`3762`) + +* `@trexfeathers`_ changed every graphics test that includes `Cartopy's coastlines`_ + to account for new adaptive coastline scaling. (:pull:`3762`) + (see also `Cartopy#1105`_) + +* `@trexfeathers`_ changed graphics tests to account for some new default + grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) + +* `@trexfeathers`_ added additional acceptable graphics test targets to account + for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and + axes borders). (:pull:`3762`) + +* `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore + `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. + (:pull:`3846`) + +* `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. + (:pull:`3866`) + +* `@lbdreyer`_ updated the CF standard name table to the latest version: `v75`_. + (:pull:`3867`) + +* `@bjlittle`_ added :pep:`517` and :pep:`518` support for building and + installing Iris, in particular to handle the `PyKE`_ package dependency. + (:pull:`3812`) + +* `@bjlittle`_ added metadata support for comparing :attr:`~iris.cube.Cube.attributes` + dictionaries that contain `numpy`_ arrays using `xxHash`_, an extremely fast + non-cryptographic hash algorithm, running at RAM speed limits. + +* `@bjlittle`_ added the ``iris.tests.assertDictEqual`` method to override + :meth:`unittest.TestCase.assertDictEqual` in order to cope with testing + metadata :attr:`~iris.cube.Cube.attributes` dictionary comparison where + the value of a key may be a `numpy`_ array. (:pull:`3785`) + +* `@bjlittle`_ added the :func:`~iris.config.get_logger` function for creating + a generic :class:`logging.Logger` with a :class:`logging.StreamHandler` and + custom :class:`logging.Formatter`. (:pull:`3785`) + +* `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header + loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) + +* `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). + +* `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). + +* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ support. (:pull:`3928`) + +* `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. + It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to + run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris + with `flake8`_ and `black`_. (:pull:`3928`) + + +.. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ +.. _Matplotlib: https://matplotlib.org/ +.. _CF units rules: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#units +.. _CF Ancillary Data: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#ancillary-data +.. _Quality Flags: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.8/cf-conventions.html#flags +.. _iris-grib: https://github.com/SciTools/iris-grib +.. _Cartopy: https://github.com/SciTools/cartopy +.. _Cartopy's coastlines: https://scitools.org.uk/cartopy/docs/latest/matplotlib/geoaxes.html?highlight=coastlines#cartopy.mpl.geoaxes.GeoAxes.coastlines +.. _Cartopy#1105: https://github.com/SciTools/cartopy/pull/1105 +.. _Cartopy#1117: https://github.com/SciTools/cartopy/pull/1117 +.. _Dask: https://github.com/dask/dask +.. _matplotlib.dates.date2num: https://matplotlib.org/api/dates_api.html#matplotlib.dates.date2num +.. _Proj: https://github.com/OSGeo/PROJ +.. _black: https://black.readthedocs.io/en/stable/ +.. _Proj#1292: https://github.com/OSGeo/PROJ/pull/1292 +.. _Proj#2151: https://github.com/OSGeo/PROJ/pull/2151 +.. _GDAL: https://github.com/OSGeo/gdal +.. _GDAL#1185: https://github.com/OSGeo/gdal/pull/1185 +.. _@MoseleyS: https://github.com/MoseleyS +.. _@stephenworsley: https://github.com/stephenworsley +.. _@pp-mo: https://github.com/pp-mo +.. _@abooton: https://github.com/abooton +.. _@bouweandela: https://github.com/bouweandela +.. _@bjlittle: https://github.com/bjlittle +.. _@trexfeathers: https://github.com/trexfeathers +.. _@jonseddon: https://github.com/jonseddon +.. _@tkknight: https://github.com/tkknight +.. _@lbdreyer: https://github.com/lbdreyer +.. _@SimonPeatman: https://github.com/SimonPeatman +.. _@TomekTrzeciak: https://github.com/TomekTrzeciak +.. _@rcomer: https://github.com/rcomer +.. _@jvegasbsc: https://github.com/jvegasbsc +.. _@zklaus: https://github.com/zklaus +.. _@znicholls: https://github.com/znicholls +.. _ESMValTool: https://github.com/ESMValGroup/ESMValTool +.. _v75: https://cfconventions.org/Data/cf-standard-names/75/build/cf-standard-name-table.html +.. _sphinx-panels: https://sphinx-panels.readthedocs.io/en/latest/ +.. _logging: https://docs.python.org/3/library/logging.html +.. _numpy: https://github.com/numpy/numpy +.. _xxHash: https://github.com/Cyan4973/xxHash +.. _PyKE: https://pypi.org/project/scitools-pyke/ +.. _matplotlib.rcdefaults: https://matplotlib.org/3.1.1/api/matplotlib_configuration_api.html?highlight=rcdefaults#matplotlib.rcdefaults +.. _@owena11: https://github.com/owena11 +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose +.. _readthedocs: https://readthedocs.org/ +.. _CF Conventions and Metadata: https://cfconventions.org/ +.. _flake8: https://flake8.pycqa.org/en/stable/ +.. _nox: https://nox.thea.codes/en/stable/ +.. _Title Case Capitalization: https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case +.. _travis-ci: https://travis-ci.org/github/SciTools/iris +.. _stickler-ci: https://stickler-ci.com/ diff --git a/docs/iris/src/whatsnew/index.rst b/docs/iris/src/whatsnew/index.rst index c7fabc0479a..257674718a5 100644 --- a/docs/iris/src/whatsnew/index.rst +++ b/docs/iris/src/whatsnew/index.rst @@ -11,6 +11,7 @@ Iris versions. :maxdepth: 1 latest.rst + 3.0.1.rst 3.0.rst 2.4.rst 2.3.rst diff --git a/lib/iris/aux_factory.py b/lib/iris/aux_factory.py index 5b63ff53ed2..962b46e9e2f 100644 --- a/lib/iris/aux_factory.py +++ b/lib/iris/aux_factory.py @@ -11,6 +11,7 @@ from abc import ABCMeta, abstractmethod import warnings +import cf_units import dask.array as da import numpy as np @@ -619,6 +620,10 @@ def _check_dependencies(delta, sigma, surface_air_pressure): warnings.warn(msg, UserWarning, stacklevel=2) # Check units. + if sigma is not None and sigma.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + sigma.units = cf_units.Unit("1") + if sigma is not None and not sigma.units.is_dimensionless(): raise ValueError("Invalid units: sigma must be dimensionless.") if ( @@ -863,6 +868,10 @@ def _check_dependencies(sigma, eta, depth, depth_c, nsigma, zlev): ) raise ValueError(msg) + if sigma is not None and sigma.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + sigma.units = cf_units.Unit("1") + if sigma is not None and not sigma.units.is_dimensionless(): msg = ( "Invalid units: sigma coordinate {!r} " @@ -1127,6 +1136,10 @@ def _check_dependencies(sigma, eta, depth): warnings.warn(msg, UserWarning, stacklevel=2) # Check units. + if sigma is not None and sigma.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + sigma.units = cf_units.Unit("1") + if sigma is not None and not sigma.units.is_dimensionless(): msg = ( "Invalid units: sigma coordinate {!r} " @@ -1335,6 +1348,10 @@ def _check_dependencies(s, c, eta, depth, depth_c): # Check units. coords = ((s, "s"), (c, "c")) for coord, term in coords: + if coord is not None and coord.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + coord.units = cf_units.Unit("1") + if coord is not None and not coord.units.is_dimensionless(): msg = ( "Invalid units: {} coordinate {!r} " @@ -1551,6 +1568,10 @@ def _check_dependencies(s, eta, depth, a, b, depth_c): raise ValueError(msg) # Check units. + if s is not None and s.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + s.units = cf_units.Unit("1") + if s is not None and not s.units.is_dimensionless(): msg = ( "Invalid units: s coordinate {!r} " @@ -1776,6 +1797,10 @@ def _check_dependencies(s, c, eta, depth, depth_c): # Check units. coords = ((s, "s"), (c, "c")) for coord, term in coords: + if coord is not None and coord.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + coord.units = cf_units.Unit("1") + if coord is not None and not coord.units.is_dimensionless(): msg = ( "Invalid units: {} coordinate {!r} " diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index 98f712a970e..bb7a870d58c 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -719,6 +719,9 @@ def coord_from_term(term): warnings.warn(msg) coord_a = coord_from_term("a") if coord_a is not None: + if coord_a.units.is_unknown(): + # Be graceful, and promote unknown to dimensionless units. + coord_a.units = "1" delta = coord_a * coord_p0.points[0] delta.units = coord_a.units * coord_p0.units delta.rename("vertical pressure") diff --git a/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py b/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py index 14944891f29..32091c7d639 100644 --- a/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py +++ b/lib/iris/tests/unit/aux_factory/test_HybridPressureFactory.py @@ -113,6 +113,15 @@ def test_factory_metadata(self): self.assertIsNone(factory.coord_system) self.assertEqual(factory.attributes, {}) + def test_promote_sigma_units_unknown_to_dimensionless(self): + sigma = mock.Mock(units=cf_units.Unit("unknown"), nbounds=0) + factory = HybridPressureFactory( + delta=self.delta, + sigma=sigma, + surface_air_pressure=self.surface_air_pressure, + ) + self.assertEqual("1", factory.dependencies["sigma"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/aux_factory/test_OceanSFactory.py b/lib/iris/tests/unit/aux_factory/test_OceanSFactory.py index caf9d303c6c..6e8e40cd1bc 100644 --- a/lib/iris/tests/unit/aux_factory/test_OceanSFactory.py +++ b/lib/iris/tests/unit/aux_factory/test_OceanSFactory.py @@ -137,6 +137,12 @@ def test_depth_incompatible_units(self): with self.assertRaises(ValueError): OceanSFactory(**self.kwargs) + def test_promote_s_units_unknown_to_dimensionless(self): + s = mock.Mock(units=Unit("unknown"), nbounds=0) + self.kwargs["s"] = s + factory = OceanSFactory(**self.kwargs) + self.assertEqual("1", factory.dependencies["s"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/aux_factory/test_OceanSg1Factory.py b/lib/iris/tests/unit/aux_factory/test_OceanSg1Factory.py index 99a4fe17327..238df2f0737 100644 --- a/lib/iris/tests/unit/aux_factory/test_OceanSg1Factory.py +++ b/lib/iris/tests/unit/aux_factory/test_OceanSg1Factory.py @@ -121,6 +121,15 @@ def test_depth_incompatible_units(self): with self.assertRaises(ValueError): OceanSg1Factory(**self.kwargs) + def test_promote_c_and_s_units_unknown_to_dimensionless(self): + c = mock.Mock(units=Unit("unknown"), nbounds=0) + s = mock.Mock(units=Unit("unknown"), nbounds=0) + self.kwargs["c"] = c + self.kwargs["s"] = s + factory = OceanSg1Factory(**self.kwargs) + self.assertEqual("1", factory.dependencies["c"].units) + self.assertEqual("1", factory.dependencies["s"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/aux_factory/test_OceanSg2Factory.py b/lib/iris/tests/unit/aux_factory/test_OceanSg2Factory.py index 387f0e48d13..fb3ada382e7 100644 --- a/lib/iris/tests/unit/aux_factory/test_OceanSg2Factory.py +++ b/lib/iris/tests/unit/aux_factory/test_OceanSg2Factory.py @@ -121,6 +121,15 @@ def test_depth_incompatible_units(self): with self.assertRaises(ValueError): OceanSg2Factory(**self.kwargs) + def test_promote_c_and_s_units_unknown_to_dimensionless(self): + c = mock.Mock(units=Unit("unknown"), nbounds=0) + s = mock.Mock(units=Unit("unknown"), nbounds=0) + self.kwargs["c"] = c + self.kwargs["s"] = s + factory = OceanSg2Factory(**self.kwargs) + self.assertEqual("1", factory.dependencies["c"].units) + self.assertEqual("1", factory.dependencies["s"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/aux_factory/test_OceanSigmaFactory.py b/lib/iris/tests/unit/aux_factory/test_OceanSigmaFactory.py index 07c970ad7ed..69a8a32c6e3 100644 --- a/lib/iris/tests/unit/aux_factory/test_OceanSigmaFactory.py +++ b/lib/iris/tests/unit/aux_factory/test_OceanSigmaFactory.py @@ -59,6 +59,12 @@ def test_depth_incompatible_units(self): with self.assertRaises(ValueError): OceanSigmaFactory(**self.kwargs) + def test_promote_sigma_units_unknown_to_dimensionless(self): + sigma = mock.Mock(units=Unit("unknown"), nbounds=0) + self.kwargs["sigma"] = sigma + factory = OceanSigmaFactory(**self.kwargs) + self.assertEqual("1", factory.dependencies["sigma"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/aux_factory/test_OceanSigmaZFactory.py b/lib/iris/tests/unit/aux_factory/test_OceanSigmaZFactory.py index 6f1e8cd57a1..4a4e30b9ca8 100644 --- a/lib/iris/tests/unit/aux_factory/test_OceanSigmaZFactory.py +++ b/lib/iris/tests/unit/aux_factory/test_OceanSigmaZFactory.py @@ -138,6 +138,12 @@ def test_depth_incompatible_units(self): with self.assertRaises(ValueError): OceanSigmaZFactory(**self.kwargs) + def test_promote_sigma_units_unknown_to_dimensionless(self): + sigma = mock.Mock(units=Unit("unknown"), nbounds=0) + self.kwargs["sigma"] = sigma + factory = OceanSigmaZFactory(**self.kwargs) + self.assertEqual("1", factory.dependencies["sigma"].units) + class Test_dependencies(tests.IrisTest): def setUp(self): diff --git a/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py b/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py index 48cc9c0d1a7..c8f9460e0f3 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test__load_aux_factory.py @@ -53,8 +53,44 @@ def test_formula_terms_ap(self): self.assertEqual(factory.surface_air_pressure, self.ps) def test_formula_terms_a_p0(self): - coord_a = DimCoord(np.arange(5), units="Pa") - coord_p0 = DimCoord(10, units="1") + coord_a = DimCoord(np.arange(5), units="1") + coord_p0 = DimCoord(10, units="Pa") + coord_expected = DimCoord( + np.arange(5) * 10, + units="Pa", + long_name="vertical pressure", + var_name="ap", + ) + self.cube_parts["coordinates"].extend( + [(coord_a, "a"), (coord_p0, "p0")] + ) + self.requires["formula_terms"] = dict(a="a", b="b", ps="ps", p0="p0") + _load_aux_factory(self.engine, self.cube) + # Check cube.coord_dims method. + self.assertEqual(self.cube.coord_dims.call_count, 1) + args, _ = self.cube.coord_dims.call_args + self.assertEqual(len(args), 1) + self.assertIs(args[0], coord_a) + # Check cube.add_aux_coord method. + self.assertEqual(self.cube.add_aux_coord.call_count, 1) + args, _ = self.cube.add_aux_coord.call_args + self.assertEqual(len(args), 2) + self.assertEqual(args[0], coord_expected) + self.assertIsInstance(args[1], mock.Mock) + # Check cube.add_aux_factory method. + self.assertEqual(self.cube.add_aux_factory.call_count, 1) + args, _ = self.cube.add_aux_factory.call_args + self.assertEqual(len(args), 1) + factory = args[0] + self.assertEqual(factory.delta, coord_expected) + self.assertEqual(factory.sigma, mock.sentinel.b) + self.assertEqual(factory.surface_air_pressure, self.ps) + + def test_formula_terms_a_p0__promote_a_units_unknown_to_dimensionless( + self, + ): + coord_a = DimCoord(np.arange(5), units="unknown") + coord_p0 = DimCoord(10, units="Pa") coord_expected = DimCoord( np.arange(5) * 10, units="Pa", @@ -71,6 +107,7 @@ def test_formula_terms_a_p0(self): args, _ = self.cube.coord_dims.call_args self.assertEqual(len(args), 1) self.assertIs(args[0], coord_a) + self.assertEqual("1", args[0].units) # Check cube.add_aux_coord method. self.assertEqual(self.cube.add_aux_coord.call_count, 1) args, _ = self.cube.add_aux_coord.call_args From c5efe72e37f8b5bef37ddc2170587e592f917c87 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Sun, 31 Jan 2021 16:21:47 +0000 Subject: [PATCH 11/23] Docs whatsnew enumerated lists (#3970) * use enumerated lists in whatsnew * add enumerated list for latest template * enumerate patch dropdown * review actions --- .../documenting/whats_new_contributions.rst | 24 +- docs/iris/src/whatsnew/3.0.1.rst | 652 +++++++++--------- docs/iris/src/whatsnew/3.0.rst | 643 ++++++++--------- docs/iris/src/whatsnew/latest.rst | 35 +- docs/iris/src/whatsnew/latest.rst.template | 16 +- 5 files changed, 687 insertions(+), 683 deletions(-) diff --git a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst index d6f805c5112..4bd90213333 100644 --- a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst @@ -59,16 +59,15 @@ what's new document. The appropriate contribution for a pull request might in fact be an addition or change to an existing "What's New" entry. -Each contribution will ideally be written as a single concise bullet point -in a reStructuredText format. Where possible do not exceed **column 80** and -ensure that any subsequent lines of the same bullet point are aligned with the -first. The content should target an Iris user as the audience. The required -content, in order, is as follows: +Each contribution will ideally be written as a single concise entry using a +reStructuredText auto-enumerated list ``#.`` directive. Where possible do not +exceed **column 80** and ensure that any subsequent lines of the same entry are +aligned with the first. The content should target an Iris user as the audience. +The required content, in order, is as follows: * Names of those who contributed the change. These should be their GitHub user name. Link the name to their GitHub profile. E.g. - ```@bjlittle `_ and - `@tkknight `_ changed...`` + ```@tkknight `_ changed...`` * The new/changed behaviour @@ -79,15 +78,14 @@ content, in order, is as follows: * Pull request references, bracketed, following the final period. E.g. ``(:pull:`1111`, :pull:`9999`)`` -* A trailing blank line (standard reStructuredText bullet format) +* A trailing blank line (standard reStructuredText list format) For example:: - * `@bjlittle `_ and - `@tkknight `_ changed changed argument ``x`` - to be optional in :class:`~iris.module.class` and - :meth:`iris.module.method`. This allows greater flexibility as requested in - :issue:`9999`. (:pull:`1111`, :pull:`9999`) + #. `@tkknight `_ changed changed argument ``x`` + to be optional in :class:`~iris.module.class` and + :meth:`iris.module.method`. This allows greater flexibility as requested in + :issue:`9999`. (:pull:`1111`, :pull:`9999`) The above example also demonstrates some of the possible syntax for including diff --git a/docs/iris/src/whatsnew/3.0.1.rst b/docs/iris/src/whatsnew/3.0.1.rst index 597e235ebea..163fe4ff3e6 100644 --- a/docs/iris/src/whatsnew/3.0.1.rst +++ b/docs/iris/src/whatsnew/3.0.1.rst @@ -18,10 +18,10 @@ This document explains the changes made to Iris for this release 💼 **Internal** - * `@bjlittle`_ gracefully promote formula terms within :mod:`~iris.aux_factory` that have ``units`` of ``unknown`` - to ``units`` of ``1`` (dimensionless), where the formula term **must** have dimensionless ``units``. Without this - graceful treatment of ``units`` the resulting :class:`~iris.cube.Cube` will **not** contain the expected auxiliary - factory, and the associated derived coordinate will be missing. (:pull:`3965`) + #. `@bjlittle`_ gracefully promote formula terms within :mod:`~iris.aux_factory` that have ``units`` of ``unknown`` + to ``units`` of ``1`` (dimensionless), where the formula term **must** have dimensionless ``units``. Without this + graceful treatment of ``units`` the resulting :class:`~iris.cube.Cube` will **not** contain the expected auxiliary + factory, and the associated derived coordinate will be missing. (:pull:`3965`) .. dropdown:: :opticon:`report` Release Highlights @@ -60,204 +60,204 @@ This document explains the changes made to Iris for this release 📢 Announcements ================ -* Congratulations to `@bouweandela`_, `@jvegasbsc`_, and `@zklaus`_ who - recently became Iris core developers. They bring a wealth of expertise to the - team, and are using Iris to underpin `ESMValTool`_ - "*A community diagnostic - and performance metrics tool for routine evaluation of Earth system models - in CMIP*". Welcome aboard! 🎉 +#. Congratulations to `@bouweandela`_, `@jvegasbsc`_, and `@zklaus`_ who + recently became Iris core developers. They bring a wealth of expertise to the + team, and are using Iris to underpin `ESMValTool`_ - "*A community diagnostic + and performance metrics tool for routine evaluation of Earth system models + in CMIP*". Welcome aboard! 🎉 -* Congratulations also goes to `@jonseddon`_ who recently became an Iris core - developer. We look forward to seeing more of your awesome contributions! 🎉 +#. Congratulations also goes to `@jonseddon`_ who recently became an Iris core + developer. We look forward to seeing more of your awesome contributions! 🎉 ✨ Features =========== -* `@MoseleyS`_ greatly enhanced the :mod:`~iris.fileformats.nimrod` - module to provide richer meta-data translation when loading ``Nimrod`` data - into cubes. This covers most known operational use-cases. (:pull:`3647`) - -* `@stephenworsley`_ improved the handling of - :class:`iris.coords.CellMeasure`\ s in the :class:`~iris.cube.Cube` - statistical operations :meth:`~iris.cube.Cube.collapsed`, - :meth:`~iris.cube.Cube.aggregated_by` and - :meth:`~iris.cube.Cube.rolling_window`. These previously removed every - :class:`~iris.coords.CellMeasure` attached to the cube. Now, a - :class:`~iris.coords.CellMeasure` will only be removed if it is associated - with an axis over which the statistic is being run. (:pull:`3549`) - -* `@stephenworsley`_, `@pp-mo`_ and `@abooton`_ added support for - `CF Ancillary Data`_ variables. These are created as - :class:`iris.coords.AncillaryVariable`, and appear as components of cubes - much like :class:`~iris.coords.AuxCoord`\ s, with the new - :class:`~iris.cube.Cube` methods - :meth:`~iris.cube.Cube.add_ancillary_variable`, - :meth:`~iris.cube.Cube.remove_ancillary_variable`, - :meth:`~iris.cube.Cube.ancillary_variable`, - :meth:`~iris.cube.Cube.ancillary_variables` and - :meth:`~iris.cube.Cube.ancillary_variable_dims`. - They are loaded from and saved to NetCDF-CF files. Special support for - `Quality Flags`_ is also provided, to ensure they load and save with - appropriate units. (:pull:`3800`) - -* `@bouweandela`_ implemented lazy regridding for the - :class:`~iris.analysis.Linear`, :class:`~iris.analysis.Nearest`, and - :class:`~iris.analysis.AreaWeighted` regridding schemes. (:pull:`3701`) - -* `@bjlittle`_ added `logging`_ support within :mod:`iris.analysis.maths`, - :mod:`iris.common.metadata`, and :mod:`iris.common.resolve`. Each module - defines a :class:`logging.Logger` instance called ``logger`` with a default - ``level`` of ``INFO``. To enable ``DEBUG`` logging use - ``logger.setLevel("DEBUG")``. (:pull:`3785`) - -* `@bjlittle`_ added the :mod:`iris.common.resolve` module, which provides - infrastructure to support the analysis, identification and combination - of metadata common between two :class:`~iris.cube.Cube` operands into a - single resultant :class:`~iris.cube.Cube` that will be auto-transposed, - and with the appropriate broadcast shape. (:pull:`3785`) - -* `@bjlittle`_ added the :ref:`common metadata API `, which provides - a unified treatment of metadata across Iris, and allows users to easily - manage and manipulate their metadata in a consistent way. (:pull:`3785`) - -* `@bjlittle`_ added :ref:`lenient metadata ` support, to - allow users to control **strict** or **lenient** metadata equivalence, - difference and combination. (:pull:`3785`) - -* `@bjlittle`_ added :ref:`lenient cube maths ` support and - resolved several long standing major issues with cube arithmetic regarding - a more robust treatment of cube broadcasting, cube dimension auto-transposition, - and preservation of common metadata and coordinates during cube math operations. - Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) - -* `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when - collapsing over a single dimension. - Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. - The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works - with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) +#. `@MoseleyS`_ greatly enhanced the :mod:`~iris.fileformats.nimrod` + module to provide richer meta-data translation when loading ``Nimrod`` data + into cubes. This covers most known operational use-cases. (:pull:`3647`) + +#. `@stephenworsley`_ improved the handling of + :class:`iris.coords.CellMeasure`\ s in the :class:`~iris.cube.Cube` + statistical operations :meth:`~iris.cube.Cube.collapsed`, + :meth:`~iris.cube.Cube.aggregated_by` and + :meth:`~iris.cube.Cube.rolling_window`. These previously removed every + :class:`~iris.coords.CellMeasure` attached to the cube. Now, a + :class:`~iris.coords.CellMeasure` will only be removed if it is associated + with an axis over which the statistic is being run. (:pull:`3549`) + +#. `@stephenworsley`_, `@pp-mo`_ and `@abooton`_ added support for + `CF Ancillary Data`_ variables. These are created as + :class:`iris.coords.AncillaryVariable`, and appear as components of cubes + much like :class:`~iris.coords.AuxCoord`\ s, with the new + :class:`~iris.cube.Cube` methods + :meth:`~iris.cube.Cube.add_ancillary_variable`, + :meth:`~iris.cube.Cube.remove_ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variables` and + :meth:`~iris.cube.Cube.ancillary_variable_dims`. + They are loaded from and saved to NetCDF-CF files. Special support for + `Quality Flags`_ is also provided, to ensure they load and save with + appropriate units. (:pull:`3800`) + +#. `@bouweandela`_ implemented lazy regridding for the + :class:`~iris.analysis.Linear`, :class:`~iris.analysis.Nearest`, and + :class:`~iris.analysis.AreaWeighted` regridding schemes. (:pull:`3701`) + +#. `@bjlittle`_ added `logging`_ support within :mod:`iris.analysis.maths`, + :mod:`iris.common.metadata`, and :mod:`iris.common.resolve`. Each module + defines a :class:`logging.Logger` instance called ``logger`` with a default + ``level`` of ``INFO``. To enable ``DEBUG`` logging use + ``logger.setLevel("DEBUG")``. (:pull:`3785`) + +#. `@bjlittle`_ added the :mod:`iris.common.resolve` module, which provides + infrastructure to support the analysis, identification and combination + of metadata common between two :class:`~iris.cube.Cube` operands into a + single resultant :class:`~iris.cube.Cube` that will be auto-transposed, + and with the appropriate broadcast shape. (:pull:`3785`) + +#. `@bjlittle`_ added the :ref:`common metadata API `, which provides + a unified treatment of metadata across Iris, and allows users to easily + manage and manipulate their metadata in a consistent way. (:pull:`3785`) + +#. `@bjlittle`_ added :ref:`lenient metadata ` support, to + allow users to control **strict** or **lenient** metadata equivalence, + difference and combination. (:pull:`3785`) + +#. `@bjlittle`_ added :ref:`lenient cube maths ` support and + resolved several long standing major issues with cube arithmetic regarding + a more robust treatment of cube broadcasting, cube dimension auto-transposition, + and preservation of common metadata and coordinates during cube math operations. + Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) + +#. `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when + collapsing over a single dimension. + Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. + The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works + with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) 🐛 Bugs Fixed ============= -* `@stephenworsley`_ fixed :meth:`~iris.cube.Cube.remove_coord` to now also - remove derived coordinates by removing aux_factories. (:pull:`3641`) - -* `@jonseddon`_ fixed ``isinstance(cube, collections.Iterable)`` to now behave - as expected if a :class:`~iris.cube.Cube` is iterated over, while also - ensuring that ``TypeError`` is still raised. (Fixed by setting the - ``__iter__()`` method in :class:`~iris.cube.Cube` to ``None``). - (:pull:`3656`) - -* `@stephenworsley`_ enabled cube concatenation along an axis shared by cell - measures; these cell measures are now concatenated together in the resulting - cube. Such a scenario would previously cause concatenation to inappropriately - fail. (:pull:`3566`) - -* `@stephenworsley`_ newly included :class:`~iris.coords.CellMeasure`\ s in - :class:`~iris.cube.Cube` copy operations. Previously copying a - :class:`~iris.cube.Cube` would ignore any attached - :class:`~iris.coords.CellMeasure`. (:pull:`3546`) - -* `@bjlittle`_ set a :class:`~iris.coords.CellMeasure`'s - ``measure`` attribute to have a default value of ``area``. - Previously, the ``measure`` was provided as a keyword argument to - :class:`~iris.coords.CellMeasure` with a default value of ``None``, which - caused a ``TypeError`` when no ``measure`` was provided, since ``area`` or - ``volume`` are the only accepted values. (:pull:`3533`) - -* `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use - `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot - axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` - did not include this behaviour). (:pull:`3762`) - -* `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to - now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ - (previously would take the unit from a time coordinate, if present, even - though the coordinate's value had been changed via ``date2num``). - (:pull:`3762`) - -* `@pp-mo`_ newly included attributes of cell measures in NETCDF-CF - file loading; they were previously being discarded. They are now available on - the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. - (:pull:`3800`) - -* `@pp-mo`_ fixed the netcdf loader to now handle any grid-mapping - variables with missing ``false_easting`` and ``false_northing`` properties, - which was previously failing for some coordinate systems. See :issue:`3629`. - (:pull:`3804`) - -* `@stephenworsley`_ changed the way tick labels are assigned from string coords. - Previously, the first tick label would occasionally be duplicated. This also - removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) - -* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check - ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) - -* `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's - coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) +#. `@stephenworsley`_ fixed :meth:`~iris.cube.Cube.remove_coord` to now also + remove derived coordinates by removing aux_factories. (:pull:`3641`) + +#. `@jonseddon`_ fixed ``isinstance(cube, collections.Iterable)`` to now behave + as expected if a :class:`~iris.cube.Cube` is iterated over, while also + ensuring that ``TypeError`` is still raised. (Fixed by setting the + ``__iter__()`` method in :class:`~iris.cube.Cube` to ``None``). + (:pull:`3656`) + +#. `@stephenworsley`_ enabled cube concatenation along an axis shared by cell + measures; these cell measures are now concatenated together in the resulting + cube. Such a scenario would previously cause concatenation to inappropriately + fail. (:pull:`3566`) + +#. `@stephenworsley`_ newly included :class:`~iris.coords.CellMeasure`\ s in + :class:`~iris.cube.Cube` copy operations. Previously copying a + :class:`~iris.cube.Cube` would ignore any attached + :class:`~iris.coords.CellMeasure`. (:pull:`3546`) + +#. `@bjlittle`_ set a :class:`~iris.coords.CellMeasure`'s + ``measure`` attribute to have a default value of ``area``. + Previously, the ``measure`` was provided as a keyword argument to + :class:`~iris.coords.CellMeasure` with a default value of ``None``, which + caused a ``TypeError`` when no ``measure`` was provided, since ``area`` or + ``volume`` are the only accepted values. (:pull:`3533`) + +#. `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use + `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot + axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` + did not include this behaviour). (:pull:`3762`) + +#. `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to + now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ + (previously would take the unit from a time coordinate, if present, even + though the coordinate's value had been changed via ``date2num``). + (:pull:`3762`) + +#. `@pp-mo`_ newly included attributes of cell measures in NETCDF-CF + file loading; they were previously being discarded. They are now available on + the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. + (:pull:`3800`) + +#. `@pp-mo`_ fixed the netcdf loader to now handle any grid-mapping + variables with missing ``false_easting`` and ``false_northing`` properties, + which was previously failing for some coordinate systems. See :issue:`3629`. + (:pull:`3804`) + +#. `@stephenworsley`_ changed the way tick labels are assigned from string coords. + Previously, the first tick label would occasionally be duplicated. This also + removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) + +#. `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check + ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) + +#. `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's + coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) .. _whatsnew 3.0.1 changes: 💣 Incompatible Changes ======================= -* `@pp-mo`_ rationalised :class:`~iris.cube.CubeList` extraction - methods: - - The former method ``iris.cube.CubeList.extract_strict``, and the ``strict`` - keyword of the :meth:`~iris.cube.CubeList.extract` method have been removed, - and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` - and :meth:`~iris.cube.CubeList.extract_cubes`. - The new routines perform the same operation, but in a style more like other - ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. - Unlike ``strict`` extraction, the type of return value is now completely - consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a - :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` - always returns an :class:`iris.cube.CubeList` of a length equal to the - number of constraints. (:pull:`3715`) - -* `@pp-mo`_ removed the former function - ``iris.analysis.coord_comparison``. (:pull:`3562`) - -* `@bjlittle`_ moved the - :func:`iris.experimental.equalise_cubes.equalise_attributes` function from - the :mod:`iris.experimental` module into the :mod:`iris.util` module. Please - use the :func:`iris.util.equalise_attributes` function instead. - (:pull:`3527`) - -* `@bjlittle`_ removed the module ``iris.experimental.concatenate``. In - ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the - :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the - :func:`iris.experimental.concatenate.concatenate` function raised an - exception. (:pull:`3523`) - -* `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` - and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) - -* `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have - ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` - property. Previously these cases defaulted to ``units='1'``. - This affects loading of coordinates whose file variable has no "units" - attribute (not valid, under `CF units rules`_): These will now have units - of `"unknown"`, rather than `"1"`, which **may prevent the creation of - a hybrid vertical coordinate**. While these cases used to "work", this was - never really correct behaviour. (:pull:`3795`) - -* `@SimonPeatman`_ added attribute ``var_name`` to coordinates created by the - :func:`iris.analysis.trajectory.interpolate` function. This prevents - duplicate coordinate errors in certain circumstances. (:pull:`3718`) - -* `@bjlittle`_ aligned the :func:`iris.analysis.maths.apply_ufunc` with the - rest of the :mod:`iris.analysis.maths` API by changing its keyword argument - from ``other_cube`` to ``other``. (:pull:`3785`) - -* `@bjlittle`_ changed the :meth:`iris.analysis.maths.IFunc.__call__` to ignore - any surplus ``other`` keyword argument for a ``data_func`` that requires - **only one** argument. This aligns the behaviour of - :meth:`iris.analysis.maths.IFunc.__call__` with - :func:`~iris.analysis.maths.apply_ufunc`. Previously a ``ValueError`` - exception was raised. (:pull:`3785`) +#. `@pp-mo`_ rationalised :class:`~iris.cube.CubeList` extraction + methods: + + The former method ``iris.cube.CubeList.extract_strict``, and the ``strict`` + keyword of the :meth:`~iris.cube.CubeList.extract` method have been removed, + and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` + and :meth:`~iris.cube.CubeList.extract_cubes`. + The new routines perform the same operation, but in a style more like other + ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. + Unlike ``strict`` extraction, the type of return value is now completely + consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a + :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` + always returns an :class:`iris.cube.CubeList` of a length equal to the + number of constraints. (:pull:`3715`) + +#. `@pp-mo`_ removed the former function + ``iris.analysis.coord_comparison``. (:pull:`3562`) + +#. `@bjlittle`_ moved the + :func:`iris.experimental.equalise_cubes.equalise_attributes` function from + the :mod:`iris.experimental` module into the :mod:`iris.util` module. Please + use the :func:`iris.util.equalise_attributes` function instead. + (:pull:`3527`) + +#. `@bjlittle`_ removed the module ``iris.experimental.concatenate``. In + ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the + :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the + :func:`iris.experimental.concatenate.concatenate` function raised an + exception. (:pull:`3523`) + +#. `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` + and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) + +#. `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have + ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` + property. Previously these cases defaulted to ``units='1'``. + This affects loading of coordinates whose file variable has no "units" + attribute (not valid, under `CF units rules`_): These will now have units + of `"unknown"`, rather than `"1"`, which **may prevent the creation of + a hybrid vertical coordinate**. While these cases used to "work", this was + never really correct behaviour. (:pull:`3795`) + +#. `@SimonPeatman`_ added attribute ``var_name`` to coordinates created by the + :func:`iris.analysis.trajectory.interpolate` function. This prevents + duplicate coordinate errors in certain circumstances. (:pull:`3718`) + +#. `@bjlittle`_ aligned the :func:`iris.analysis.maths.apply_ufunc` with the + rest of the :mod:`iris.analysis.maths` API by changing its keyword argument + from ``other_cube`` to ``other``. (:pull:`3785`) + +#. `@bjlittle`_ changed the :meth:`iris.analysis.maths.IFunc.__call__` to ignore + any surplus ``other`` keyword argument for a ``data_func`` that requires + **only one** argument. This aligns the behaviour of + :meth:`iris.analysis.maths.IFunc.__call__` with + :func:`~iris.analysis.maths.apply_ufunc`. Previously a ``ValueError`` + exception was raised. (:pull:`3785`) .. _whatsnew 3.0.1 deprecations: @@ -265,48 +265,48 @@ This document explains the changes made to Iris for this release 🔥 Deprecations =============== -* `@stephenworsley`_ removed the deprecated :class:`iris.Future` flags - ``cell_date_time_objects``, ``netcdf_promote``, ``netcdf_no_unlimited`` and - ``clip_latitudes``. (:pull:`3459`) +#. `@stephenworsley`_ removed the deprecated :class:`iris.Future` flags + ``cell_date_time_objects``, ``netcdf_promote``, ``netcdf_no_unlimited`` and + ``clip_latitudes``. (:pull:`3459`) -* `@stephenworsley`_ changed :attr:`iris.fileformats.pp.PPField.lbproc` to be an - ``int``. The deprecated attributes ``flag1``, ``flag2`` etc. have been - removed from it. (:pull:`3461`) +#. `@stephenworsley`_ changed :attr:`iris.fileformats.pp.PPField.lbproc` to be an + ``int``. The deprecated attributes ``flag1``, ``flag2`` etc. have been + removed from it. (:pull:`3461`) -* `@bjlittle`_ deprecated :func:`~iris.util.as_compatible_shape` in preference - for :class:`~iris.common.resolve.Resolve` e.g., ``Resolve(src, tgt)(tgt.core_data())``. - The :func:`~iris.util.as_compatible_shape` function will be removed in a future - release of Iris. (:pull:`3892`) +#. `@bjlittle`_ deprecated :func:`~iris.util.as_compatible_shape` in preference + for :class:`~iris.common.resolve.Resolve` e.g., ``Resolve(src, tgt)(tgt.core_data())``. + The :func:`~iris.util.as_compatible_shape` function will be removed in a future + release of Iris. (:pull:`3892`) 🔗 Dependencies =============== -* `@stephenworsley`_, `@trexfeathers`_ and `@bjlittle`_ removed ``Python2`` - support, modernising the codebase by switching to exclusive ``Python3`` - support. (:pull:`3513`) +#. `@stephenworsley`_, `@trexfeathers`_ and `@bjlittle`_ removed ``Python2`` + support, modernising the codebase by switching to exclusive ``Python3`` + support. (:pull:`3513`) -* `@bjlittle`_ improved the developer set up process. Configuring Iris and - :ref:`installing_from_source` as a developer with all the required package - dependencies is now easier with our curated conda environment YAML files. - (:pull:`3812`) +#. `@bjlittle`_ improved the developer set up process. Configuring Iris and + :ref:`installing_from_source` as a developer with all the required package + dependencies is now easier with our curated conda environment YAML files. + (:pull:`3812`) -* `@stephenworsley`_ pinned Iris to require `Dask`_ ``>=2.0``. (:pull:`3460`) +#. `@stephenworsley`_ pinned Iris to require `Dask`_ ``>=2.0``. (:pull:`3460`) -* `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require - `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version - of `Matplotlib`_. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require + `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version + of `Matplotlib`_. (:pull:`3762`) -* `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. - Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in - pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer - necessary now that ``Python2`` support has been dropped. (:pull:`3468`) +#. `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. + Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in + pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer + necessary now that ``Python2`` support has been dropped. (:pull:`3468`) -* `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version - of `Proj`_. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version + of `Proj`_. (:pull:`3762`) -* `@stephenworsley`_ and `@trexfeathers`_ removed GDAL from the extensions - dependency group. We no longer consider it to be an extension. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ removed GDAL from the extensions + dependency group. We no longer consider it to be an extension. (:pull:`3762`) .. _whatsnew 3.0.1 docs: @@ -314,158 +314,160 @@ This document explains the changes made to Iris for this release 📚 Documentation ================ -* `@tkknight`_ moved the - :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` - from the general part of the gallery to oceanography. (:pull:`3761`) +#. `@tkknight`_ moved the + :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` + from the general part of the gallery to oceanography. (:pull:`3761`) -* `@tkknight`_ updated documentation to use a modern sphinx theme and be - served from https://scitools-iris.readthedocs.io/en/latest/. (:pull:`3752`) +#. `@tkknight`_ updated documentation to use a modern sphinx theme and be + served from https://scitools-iris.readthedocs.io/en/latest/. (:pull:`3752`) -* `@bjlittle`_ added support for the `black`_ code formatter. This is - now automatically checked on GitHub PRs, replacing the older, unittest-based - ``iris.tests.test_coding_standards.TestCodeFormat``. Black provides automatic - code format correction for most IDEs. See the new developer guide section on - :ref:`code_formatting`. (:pull:`3518`) +#. `@bjlittle`_ added support for the `black`_ code formatter. This is + now automatically checked on GitHub PRs, replacing the older, unittest-based + ``iris.tests.test_coding_standards.TestCodeFormat``. Black provides automatic + code format correction for most IDEs. See the new developer guide section on + :ref:`code_formatting`. (:pull:`3518`) -* `@tkknight`_ and `@trexfeathers`_ refreshed the :ref:`whats_new_contributions` - for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` - what's new page so it appears on the latest documentation at - https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves - :issue:`2104`, :issue:`3451`, :issue:`3818`, :issue:`3837`. Also updated the - :ref:`iris_development_releases_steps` to follow when making a release. - (:pull:`3769`, :pull:`3838`, :pull:`3843`) +#. `@tkknight`_ and `@trexfeathers`_ refreshed the :ref:`whats_new_contributions` + for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` + what's new page so it appears on the latest documentation at + https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves + :issue:`2104`, :issue:`3451`, :issue:`3818`, :issue:`3837`. Also updated the + :ref:`iris_development_releases_steps` to follow when making a release. + (:pull:`3769`, :pull:`3838`, :pull:`3843`) -* `@tkknight`_ enabled the PDF creation of the documentation on the - `Read the Docs`_ service. The PDF may be accessed by clicking on the version - at the bottom of the side bar, then selecting ``PDF`` from the ``Downloads`` - section. (:pull:`3765`) +#. `@tkknight`_ enabled the PDF creation of the documentation on the + `Read the Docs`_ service. The PDF may be accessed by clicking on the version + at the bottom of the side bar, then selecting ``PDF`` from the ``Downloads`` + section. (:pull:`3765`) -* `@stephenworsley`_ added a warning to the - :func:`iris.analysis.cartography.project` function regarding its behaviour on - projections with non-rectangular boundaries. (:pull:`3762`) +#. `@stephenworsley`_ added a warning to the + :func:`iris.analysis.cartography.project` function regarding its behaviour on + projections with non-rectangular boundaries. (:pull:`3762`) -* `@stephenworsley`_ added the :ref:`cube_maths_combining_units` section to the - user guide to clarify how ``Units`` are handled during cube arithmetic. - (:pull:`3803`) +#. `@stephenworsley`_ added the :ref:`cube_maths_combining_units` section to the + user guide to clarify how ``Units`` are handled during cube arithmetic. + (:pull:`3803`) -* `@tkknight`_ overhauled the :ref:`developers_guide` including information on - getting involved in becoming a contributor and general structure of the - guide. This resolves :issue:`2170`, :issue:`2331`, :issue:`3453`, - :issue:`314`, :issue:`2902`. (:pull:`3852`) +#. `@tkknight`_ overhauled the :ref:`developers_guide` including information on + getting involved in becoming a contributor and general structure of the + guide. This resolves :issue:`2170`, :issue:`2331`, :issue:`3453`, + :issue:`314`, :issue:`2902`. (:pull:`3852`) -* `@rcomer`_ added argument descriptions to the :class:`~iris.coords.DimCoord` - docstring. (:pull:`3681`) +#. `@rcomer`_ added argument descriptions to the :class:`~iris.coords.DimCoord` + docstring. (:pull:`3681`) -* `@tkknight`_ added two url's to be ignored for the ``make linkcheck``. This - will ensure the Iris github project is not repeatedly hit during the - linkcheck for issues and pull requests as it can result in connection - refused and thus travis-ci_ job failures. For more information on linkcheck, - see :ref:`contributing.documentation.testing`. (:pull:`3873`) +#. `@tkknight`_ added two url's to be ignored for the ``make linkcheck``. This + will ensure the Iris github project is not repeatedly hit during the + linkcheck for issues and pull requests as it can result in connection + refused and thus travis-ci_ job failures. For more information on linkcheck, + see :ref:`contributing.documentation.testing`. (:pull:`3873`) -* `@tkknight`_ enabled the napolean_ package that is used by sphinx_ to cater - for the existing google style docstrings and to also allow for `numpy`_ - docstrings. This resolves :issue:`3841`. (:pull:`3871`) +#. `@tkknight`_ enabled the napolean_ package that is used by sphinx_ to cater + for the existing google style docstrings and to also allow for `numpy`_ + docstrings. This resolves :issue:`3841`. (:pull:`3871`) -* `@tkknight`_ configured ``sphinx-build`` to promote warnings to errors when - building the documentation via ``make html``. This will minimise technical - debt accruing for the documentation. (:pull:`3877`) +#. `@tkknight`_ configured ``sphinx-build`` to promote warnings to errors when + building the documentation via ``make html``. This will minimise technical + debt accruing for the documentation. (:pull:`3877`) -* `@tkknight`_ updated :ref:`installing_iris` to include a reference to - Windows Subsystem for Linux. (:pull:`3885`) +#. `@tkknight`_ updated :ref:`installing_iris` to include a reference to + Windows Subsystem for Linux. (:pull:`3885`) -* `@tkknight`_ updated the :ref:`iris_docs` homepage to include panels so the - links are more visible to users. This uses the sphinx-panels_ extension. - (:pull:`3884`) +#. `@tkknight`_ updated the :ref:`iris_docs` homepage to include panels so the + links are more visible to users. This uses the sphinx-panels_ extension. + (:pull:`3884`) -* `@bjlittle`_ created the :ref:`Further topics ` section and - included documentation for :ref:`metadata`, :ref:`lenient metadata`, and - :ref:`lenient maths`. (:pull:`3890`) +#. `@bjlittle`_ created the :ref:`Further topics ` section and + included documentation for :ref:`metadata`, :ref:`lenient metadata`, and + :ref:`lenient maths`. (:pull:`3890`) -* `@jonseddon`_ updated the CF version of the netCDF saver in the - :ref:`saving_iris_cubes` section and in the equivalent function docstring. - (:pull:`3925`) +#. `@jonseddon`_ updated the CF version of the netCDF saver in the + :ref:`saving_iris_cubes` section and in the equivalent function docstring. + (:pull:`3925`) -* `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. - (:pull:`3940`) +#. `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. + (:pull:`3940`) 💼 Internal =========== -* `@pp-mo`_ and `@lbdreyer`_ removed all Iris test dependencies on `iris-grib`_ - by transferring all relevant content to the `iris-grib`_ repository. (:pull:`3662`, - :pull:`3663`, :pull:`3664`, :pull:`3665`, :pull:`3666`, :pull:`3669`, - :pull:`3670`, :pull:`3671`, :pull:`3672`, :pull:`3742`, :pull:`3746`) +#. `@pp-mo`_ and `@lbdreyer`_ removed all Iris test dependencies on `iris-grib`_ + by transferring all relevant content to the `iris-grib`_ repository. (:pull:`3662`, + :pull:`3663`, :pull:`3664`, :pull:`3665`, :pull:`3666`, :pull:`3669`, + :pull:`3670`, :pull:`3671`, :pull:`3672`, :pull:`3742`, :pull:`3746`) -* `@lbdreyer`_ and `@pp-mo`_ overhauled the handling of dimensional - metadata to remove duplication. (:pull:`3422`, :pull:`3551`) +#. `@lbdreyer`_ and `@pp-mo`_ overhauled the handling of dimensional + metadata to remove duplication. (:pull:`3422`, :pull:`3551`) -* `@trexfeathers`_ simplified the standard license header for all files, which - removes the need to repeatedly update year numbers in the header. - (:pull:`3489`) +#. `@trexfeathers`_ simplified the standard license header for all files, which + removes the need to repeatedly update year numbers in the header. + (:pull:`3489`) -* `@stephenworsley`_ changed the numerical values in tests involving the - Robinson projection due to improvements made in - `Proj`_. (:pull:`3762`) (see also `Proj#1292`_ and `Proj#2151`_) +#. `@stephenworsley`_ changed the numerical values in tests involving the + Robinson projection due to improvements made in + `Proj`_. (:pull:`3762`) (see also `Proj#1292`_ and `Proj#2151`_) -* `@stephenworsley`_ changed tests to account for more detailed descriptions of - projections in `GDAL`_. (:pull:`3762`) (see also `GDAL#1185`_) +#. `@stephenworsley`_ changed tests to account for more detailed descriptions of + projections in `GDAL`_. (:pull:`3762`) (see also `GDAL#1185`_) -* `@stephenworsley`_ changed tests to account for `GDAL`_ now saving fill values - for data without masked points. (:pull:`3762`) +#. `@stephenworsley`_ changed tests to account for `GDAL`_ now saving fill values + for data without masked points. (:pull:`3762`) -* `@trexfeathers`_ changed every graphics test that includes `Cartopy's coastlines`_ - to account for new adaptive coastline scaling. (:pull:`3762`) - (see also `Cartopy#1105`_) +#. `@trexfeathers`_ changed every graphics test that includes `Cartopy's coastlines`_ + to account for new adaptive coastline scaling. (:pull:`3762`) + (see also `Cartopy#1105`_) -* `@trexfeathers`_ changed graphics tests to account for some new default - grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) +#. `@trexfeathers`_ changed graphics tests to account for some new default + grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) -* `@trexfeathers`_ added additional acceptable graphics test targets to account - for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and - axes borders). (:pull:`3762`) +#. `@trexfeathers`_ added additional acceptable graphics test targets to account + for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and + axes borders). (:pull:`3762`) -* `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore - `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. - (:pull:`3846`) +#. `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore + `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. + (:pull:`3846`) -* `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. - (:pull:`3866`) +#. `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. + (:pull:`3866`) -* `@lbdreyer`_ updated the CF standard name table to the latest version: `v75`_. - (:pull:`3867`) +#. `@lbdreyer`_ updated the CF standard name table to the latest version: `v75`_. + (:pull:`3867`) -* `@bjlittle`_ added :pep:`517` and :pep:`518` support for building and - installing Iris, in particular to handle the `PyKE`_ package dependency. - (:pull:`3812`) +#. `@bjlittle`_ added :pep:`517` and :pep:`518` support for building and + installing Iris, in particular to handle the `PyKE`_ package dependency. + (:pull:`3812`) -* `@bjlittle`_ added metadata support for comparing :attr:`~iris.cube.Cube.attributes` - dictionaries that contain `numpy`_ arrays using `xxHash`_, an extremely fast - non-cryptographic hash algorithm, running at RAM speed limits. +#. `@bjlittle`_ added metadata support for comparing :attr:`~iris.cube.Cube.attributes` + dictionaries that contain `numpy`_ arrays using `xxHash`_, an extremely fast + non-cryptographic hash algorithm, running at RAM speed limits. -* `@bjlittle`_ added the ``iris.tests.assertDictEqual`` method to override - :meth:`unittest.TestCase.assertDictEqual` in order to cope with testing - metadata :attr:`~iris.cube.Cube.attributes` dictionary comparison where - the value of a key may be a `numpy`_ array. (:pull:`3785`) +#. `@bjlittle`_ added the ``iris.tests.assertDictEqual`` method to override + :meth:`unittest.TestCase.assertDictEqual` in order to cope with testing + metadata :attr:`~iris.cube.Cube.attributes` dictionary comparison where + the value of a key may be a `numpy`_ array. (:pull:`3785`) -* `@bjlittle`_ added the :func:`~iris.config.get_logger` function for creating - a generic :class:`logging.Logger` with a :class:`logging.StreamHandler` and - custom :class:`logging.Formatter`. (:pull:`3785`) +#. `@bjlittle`_ added the :func:`~iris.config.get_logger` function for creating + a generic :class:`logging.Logger` with a :class:`logging.StreamHandler` and + custom :class:`logging.Formatter`. (:pull:`3785`) -* `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header - loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) +#. `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header + loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) -* `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). +#. `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, + see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). -* `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). +#. `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in + the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). -* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ support. (:pull:`3928`) - -* `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. - It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to - run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris - with `flake8`_ and `black`_. (:pull:`3928`) +#. `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ + support. (:pull:`3928`) +#. `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. + It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to + run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris + with `flake8`_ and `black`_. (:pull:`3928`) .. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ .. _Matplotlib: https://matplotlib.org/ diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/iris/src/whatsnew/3.0.rst index 399325add52..0f61d620331 100644 --- a/docs/iris/src/whatsnew/3.0.rst +++ b/docs/iris/src/whatsnew/3.0.rst @@ -43,204 +43,204 @@ This document explains the changes made to Iris for this release 📢 Announcements ================ -* Congratulations to `@bouweandela`_, `@jvegasbsc`_, and `@zklaus`_ who - recently became Iris core developers. They bring a wealth of expertise to the - team, and are using Iris to underpin `ESMValTool`_ - "*A community diagnostic - and performance metrics tool for routine evaluation of Earth system models - in CMIP*". Welcome aboard! 🎉 +#. Congratulations to `@bouweandela`_, `@jvegasbsc`_, and `@zklaus`_ who + recently became Iris core developers. They bring a wealth of expertise to the + team, and are using Iris to underpin `ESMValTool`_ - "*A community diagnostic + and performance metrics tool for routine evaluation of Earth system models + in CMIP*". Welcome aboard! 🎉 -* Congratulations also goes to `@jonseddon`_ who recently became an Iris core - developer. We look forward to seeing more of your awesome contributions! 🎉 +#. Congratulations also goes to `@jonseddon`_ who recently became an Iris core + developer. We look forward to seeing more of your awesome contributions! 🎉 ✨ Features =========== -* `@MoseleyS`_ greatly enhanced the :mod:`~iris.fileformats.nimrod` - module to provide richer meta-data translation when loading ``Nimrod`` data - into cubes. This covers most known operational use-cases. (:pull:`3647`) - -* `@stephenworsley`_ improved the handling of - :class:`iris.coords.CellMeasure`\ s in the :class:`~iris.cube.Cube` - statistical operations :meth:`~iris.cube.Cube.collapsed`, - :meth:`~iris.cube.Cube.aggregated_by` and - :meth:`~iris.cube.Cube.rolling_window`. These previously removed every - :class:`~iris.coords.CellMeasure` attached to the cube. Now, a - :class:`~iris.coords.CellMeasure` will only be removed if it is associated - with an axis over which the statistic is being run. (:pull:`3549`) - -* `@stephenworsley`_, `@pp-mo`_ and `@abooton`_ added support for - `CF Ancillary Data`_ variables. These are created as - :class:`iris.coords.AncillaryVariable`, and appear as components of cubes - much like :class:`~iris.coords.AuxCoord`\ s, with the new - :class:`~iris.cube.Cube` methods - :meth:`~iris.cube.Cube.add_ancillary_variable`, - :meth:`~iris.cube.Cube.remove_ancillary_variable`, - :meth:`~iris.cube.Cube.ancillary_variable`, - :meth:`~iris.cube.Cube.ancillary_variables` and - :meth:`~iris.cube.Cube.ancillary_variable_dims`. - They are loaded from and saved to NetCDF-CF files. Special support for - `Quality Flags`_ is also provided, to ensure they load and save with - appropriate units. (:pull:`3800`) - -* `@bouweandela`_ implemented lazy regridding for the - :class:`~iris.analysis.Linear`, :class:`~iris.analysis.Nearest`, and - :class:`~iris.analysis.AreaWeighted` regridding schemes. (:pull:`3701`) - -* `@bjlittle`_ added `logging`_ support within :mod:`iris.analysis.maths`, - :mod:`iris.common.metadata`, and :mod:`iris.common.resolve`. Each module - defines a :class:`logging.Logger` instance called ``logger`` with a default - ``level`` of ``INFO``. To enable ``DEBUG`` logging use - ``logger.setLevel("DEBUG")``. (:pull:`3785`) - -* `@bjlittle`_ added the :mod:`iris.common.resolve` module, which provides - infrastructure to support the analysis, identification and combination - of metadata common between two :class:`~iris.cube.Cube` operands into a - single resultant :class:`~iris.cube.Cube` that will be auto-transposed, - and with the appropriate broadcast shape. (:pull:`3785`) - -* `@bjlittle`_ added the :ref:`common metadata API `, which provides - a unified treatment of metadata across Iris, and allows users to easily - manage and manipulate their metadata in a consistent way. (:pull:`3785`) - -* `@bjlittle`_ added :ref:`lenient metadata ` support, to - allow users to control **strict** or **lenient** metadata equivalence, - difference and combination. (:pull:`3785`) - -* `@bjlittle`_ added :ref:`lenient cube maths ` support and - resolved several long standing major issues with cube arithmetic regarding - a more robust treatment of cube broadcasting, cube dimension auto-transposition, - and preservation of common metadata and coordinates during cube math operations. - Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) - -* `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when - collapsing over a single dimension. - Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. - The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works - with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) +#. `@MoseleyS`_ greatly enhanced the :mod:`~iris.fileformats.nimrod` + module to provide richer meta-data translation when loading ``Nimrod`` data + into cubes. This covers most known operational use-cases. (:pull:`3647`) + +#. `@stephenworsley`_ improved the handling of + :class:`iris.coords.CellMeasure`\ s in the :class:`~iris.cube.Cube` + statistical operations :meth:`~iris.cube.Cube.collapsed`, + :meth:`~iris.cube.Cube.aggregated_by` and + :meth:`~iris.cube.Cube.rolling_window`. These previously removed every + :class:`~iris.coords.CellMeasure` attached to the cube. Now, a + :class:`~iris.coords.CellMeasure` will only be removed if it is associated + with an axis over which the statistic is being run. (:pull:`3549`) + +#. `@stephenworsley`_, `@pp-mo`_ and `@abooton`_ added support for + `CF Ancillary Data`_ variables. These are created as + :class:`iris.coords.AncillaryVariable`, and appear as components of cubes + much like :class:`~iris.coords.AuxCoord`\ s, with the new + :class:`~iris.cube.Cube` methods + :meth:`~iris.cube.Cube.add_ancillary_variable`, + :meth:`~iris.cube.Cube.remove_ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variable`, + :meth:`~iris.cube.Cube.ancillary_variables` and + :meth:`~iris.cube.Cube.ancillary_variable_dims`. + They are loaded from and saved to NetCDF-CF files. Special support for + `Quality Flags`_ is also provided, to ensure they load and save with + appropriate units. (:pull:`3800`) + +#. `@bouweandela`_ implemented lazy regridding for the + :class:`~iris.analysis.Linear`, :class:`~iris.analysis.Nearest`, and + :class:`~iris.analysis.AreaWeighted` regridding schemes. (:pull:`3701`) + +#. `@bjlittle`_ added `logging`_ support within :mod:`iris.analysis.maths`, + :mod:`iris.common.metadata`, and :mod:`iris.common.resolve`. Each module + defines a :class:`logging.Logger` instance called ``logger`` with a default + ``level`` of ``INFO``. To enable ``DEBUG`` logging use + ``logger.setLevel("DEBUG")``. (:pull:`3785`) + +#. `@bjlittle`_ added the :mod:`iris.common.resolve` module, which provides + infrastructure to support the analysis, identification and combination + of metadata common between two :class:`~iris.cube.Cube` operands into a + single resultant :class:`~iris.cube.Cube` that will be auto-transposed, + and with the appropriate broadcast shape. (:pull:`3785`) + +#. `@bjlittle`_ added the :ref:`common metadata API `, which provides + a unified treatment of metadata across Iris, and allows users to easily + manage and manipulate their metadata in a consistent way. (:pull:`3785`) + +#. `@bjlittle`_ added :ref:`lenient metadata ` support, to + allow users to control **strict** or **lenient** metadata equivalence, + difference and combination. (:pull:`3785`) + +#. `@bjlittle`_ added :ref:`lenient cube maths ` support and + resolved several long standing major issues with cube arithmetic regarding + a more robust treatment of cube broadcasting, cube dimension auto-transposition, + and preservation of common metadata and coordinates during cube math operations. + Resolves :issue:`1887`, :issue:`2765`, and :issue:`3478`. (:pull:`3785`) + +#. `@pp-mo`_ and `@TomekTrzeciak`_ enhanced :meth:`~iris.cube.Cube.collapse` to allow a 1-D weights array when + collapsing over a single dimension. + Previously, the weights had to be the same shape as the whole cube, which could cost a lot of memory in some cases. + The 1-D form is supported by most weighted array statistics (such as :meth:`np.average`), so this now works + with the corresponding Iris schemes (in that case, :const:`~iris.analysis.MEAN`). (:pull:`3943`) 🐛 Bugs Fixed ============= -* `@stephenworsley`_ fixed :meth:`~iris.cube.Cube.remove_coord` to now also - remove derived coordinates by removing aux_factories. (:pull:`3641`) - -* `@jonseddon`_ fixed ``isinstance(cube, collections.Iterable)`` to now behave - as expected if a :class:`~iris.cube.Cube` is iterated over, while also - ensuring that ``TypeError`` is still raised. (Fixed by setting the - ``__iter__()`` method in :class:`~iris.cube.Cube` to ``None``). - (:pull:`3656`) - -* `@stephenworsley`_ enabled cube concatenation along an axis shared by cell - measures; these cell measures are now concatenated together in the resulting - cube. Such a scenario would previously cause concatenation to inappropriately - fail. (:pull:`3566`) - -* `@stephenworsley`_ newly included :class:`~iris.coords.CellMeasure`\ s in - :class:`~iris.cube.Cube` copy operations. Previously copying a - :class:`~iris.cube.Cube` would ignore any attached - :class:`~iris.coords.CellMeasure`. (:pull:`3546`) - -* `@bjlittle`_ set a :class:`~iris.coords.CellMeasure`'s - ``measure`` attribute to have a default value of ``area``. - Previously, the ``measure`` was provided as a keyword argument to - :class:`~iris.coords.CellMeasure` with a default value of ``None``, which - caused a ``TypeError`` when no ``measure`` was provided, since ``area`` or - ``volume`` are the only accepted values. (:pull:`3533`) - -* `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use - `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot - axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` - did not include this behaviour). (:pull:`3762`) - -* `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to - now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ - (previously would take the unit from a time coordinate, if present, even - though the coordinate's value had been changed via ``date2num``). - (:pull:`3762`) - -* `@pp-mo`_ newly included attributes of cell measures in NETCDF-CF - file loading; they were previously being discarded. They are now available on - the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. - (:pull:`3800`) - -* `@pp-mo`_ fixed the netcdf loader to now handle any grid-mapping - variables with missing ``false_easting`` and ``false_northing`` properties, - which was previously failing for some coordinate systems. See :issue:`3629`. - (:pull:`3804`) - -* `@stephenworsley`_ changed the way tick labels are assigned from string coords. - Previously, the first tick label would occasionally be duplicated. This also - removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) - -* `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check - ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) - -* `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's - coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) +#. `@stephenworsley`_ fixed :meth:`~iris.cube.Cube.remove_coord` to now also + remove derived coordinates by removing aux_factories. (:pull:`3641`) + +#. `@jonseddon`_ fixed ``isinstance(cube, collections.Iterable)`` to now behave + as expected if a :class:`~iris.cube.Cube` is iterated over, while also + ensuring that ``TypeError`` is still raised. (Fixed by setting the + ``__iter__()`` method in :class:`~iris.cube.Cube` to ``None``). + (:pull:`3656`) + +#. `@stephenworsley`_ enabled cube concatenation along an axis shared by cell + measures; these cell measures are now concatenated together in the resulting + cube. Such a scenario would previously cause concatenation to inappropriately + fail. (:pull:`3566`) + +#. `@stephenworsley`_ newly included :class:`~iris.coords.CellMeasure`\ s in + :class:`~iris.cube.Cube` copy operations. Previously copying a + :class:`~iris.cube.Cube` would ignore any attached + :class:`~iris.coords.CellMeasure`. (:pull:`3546`) + +#. `@bjlittle`_ set a :class:`~iris.coords.CellMeasure`'s + ``measure`` attribute to have a default value of ``area``. + Previously, the ``measure`` was provided as a keyword argument to + :class:`~iris.coords.CellMeasure` with a default value of ``None``, which + caused a ``TypeError`` when no ``measure`` was provided, since ``area`` or + ``volume`` are the only accepted values. (:pull:`3533`) + +#. `@trexfeathers`_ set **all** plot types in :mod:`iris.plot` to now use + `matplotlib.dates.date2num`_ to format date/time coordinates for use on a plot + axis (previously :meth:`~iris.plot.pcolor` and :meth:`~iris.plot.pcolormesh` + did not include this behaviour). (:pull:`3762`) + +#. `@trexfeathers`_ changed date/time axis labels in :mod:`iris.quickplot` to + now **always** be based on the ``epoch`` used in `matplotlib.dates.date2num`_ + (previously would take the unit from a time coordinate, if present, even + though the coordinate's value had been changed via ``date2num``). + (:pull:`3762`) + +#. `@pp-mo`_ newly included attributes of cell measures in NETCDF-CF + file loading; they were previously being discarded. They are now available on + the :class:`~iris.coords.CellMeasure` in the loaded :class:`~iris.cube.Cube`. + (:pull:`3800`) + +#. `@pp-mo`_ fixed the netcdf loader to now handle any grid-mapping + variables with missing ``false_easting`` and ``false_northing`` properties, + which was previously failing for some coordinate systems. See :issue:`3629`. + (:pull:`3804`) + +#. `@stephenworsley`_ changed the way tick labels are assigned from string coords. + Previously, the first tick label would occasionally be duplicated. This also + removes the use of Matplotlib's deprecated ``IndexFormatter``. (:pull:`3857`) + +#. `@znicholls`_ fixed :meth:`~iris.quickplot._title` to only check + ``units.is_time_reference`` if the ``units`` symbol is not used. (:pull:`3902`) + +#. `@rcomer`_ fixed a bug whereby numpy array type attributes on a cube's + coordinates could prevent printing it. See :issue:`3921`. (:pull:`3922`) .. _whatsnew 3.0 changes: 💣 Incompatible Changes ======================= -* `@pp-mo`_ rationalised :class:`~iris.cube.CubeList` extraction - methods: - - The former method ``iris.cube.CubeList.extract_strict``, and the ``strict`` - keyword of the :meth:`~iris.cube.CubeList.extract` method have been removed, - and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` - and :meth:`~iris.cube.CubeList.extract_cubes`. - The new routines perform the same operation, but in a style more like other - ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. - Unlike ``strict`` extraction, the type of return value is now completely - consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a - :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` - always returns an :class:`iris.cube.CubeList` of a length equal to the - number of constraints. (:pull:`3715`) - -* `@pp-mo`_ removed the former function - ``iris.analysis.coord_comparison``. (:pull:`3562`) - -* `@bjlittle`_ moved the - :func:`iris.experimental.equalise_cubes.equalise_attributes` function from - the :mod:`iris.experimental` module into the :mod:`iris.util` module. Please - use the :func:`iris.util.equalise_attributes` function instead. - (:pull:`3527`) - -* `@bjlittle`_ removed the module ``iris.experimental.concatenate``. In - ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the - :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the - :func:`iris.experimental.concatenate.concatenate` function raised an - exception. (:pull:`3523`) - -* `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` - and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) - -* `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have - ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` - property. Previously these cases defaulted to ``units='1'``. - This affects loading of coordinates whose file variable has no "units" - attribute (not valid, under `CF units rules`_): These will now have units - of `"unknown"`, rather than `"1"`, which **may prevent the creation of - a hybrid vertical coordinate**. While these cases used to "work", this was - never really correct behaviour. (:pull:`3795`) - -* `@SimonPeatman`_ added attribute ``var_name`` to coordinates created by the - :func:`iris.analysis.trajectory.interpolate` function. This prevents - duplicate coordinate errors in certain circumstances. (:pull:`3718`) - -* `@bjlittle`_ aligned the :func:`iris.analysis.maths.apply_ufunc` with the - rest of the :mod:`iris.analysis.maths` API by changing its keyword argument - from ``other_cube`` to ``other``. (:pull:`3785`) - -* `@bjlittle`_ changed the :meth:`iris.analysis.maths.IFunc.__call__` to ignore - any surplus ``other`` keyword argument for a ``data_func`` that requires - **only one** argument. This aligns the behaviour of - :meth:`iris.analysis.maths.IFunc.__call__` with - :func:`~iris.analysis.maths.apply_ufunc`. Previously a ``ValueError`` - exception was raised. (:pull:`3785`) +#. `@pp-mo`_ rationalised :class:`~iris.cube.CubeList` extraction + methods: + + The former method ``iris.cube.CubeList.extract_strict``, and the ``strict`` + keyword of the :meth:`~iris.cube.CubeList.extract` method have been removed, + and are replaced by the new routines :meth:`~iris.cube.CubeList.extract_cube` + and :meth:`~iris.cube.CubeList.extract_cubes`. + The new routines perform the same operation, but in a style more like other + ``Iris`` functions such as :meth:`~iris.load_cube` and :meth:`~iris.load_cubes`. + Unlike ``strict`` extraction, the type of return value is now completely + consistent : :meth:`~iris.cube.CubeList.extract_cube` always returns a + :class:`~iris.cube.Cube`, and :meth:`~iris.cube.CubeList.extract_cubes` + always returns an :class:`iris.cube.CubeList` of a length equal to the + number of constraints. (:pull:`3715`) + +#. `@pp-mo`_ removed the former function + ``iris.analysis.coord_comparison``. (:pull:`3562`) + +#. `@bjlittle`_ moved the + :func:`iris.experimental.equalise_cubes.equalise_attributes` function from + the :mod:`iris.experimental` module into the :mod:`iris.util` module. Please + use the :func:`iris.util.equalise_attributes` function instead. + (:pull:`3527`) + +#. `@bjlittle`_ removed the module ``iris.experimental.concatenate``. In + ``v1.6.0`` the experimental ``concatenate`` functionality was moved to the + :meth:`iris.cube.CubeList.concatenate` method. Since then, calling the + :func:`iris.experimental.concatenate.concatenate` function raised an + exception. (:pull:`3523`) + +#. `@stephenworsley`_ changed the default units of :class:`~iris.coords.DimCoord` + and :class:`~iris.coords.AuxCoord` from `"1"` to `"unknown"`. (:pull:`3795`) + +#. `@stephenworsley`_ changed Iris objects loaded from NetCDF-CF files to have + ``units='unknown'`` where the corresponding NetCDF variable has no ``units`` + property. Previously these cases defaulted to ``units='1'``. + This affects loading of coordinates whose file variable has no "units" + attribute (not valid, under `CF units rules`_): These will now have units + of `"unknown"`, rather than `"1"`, which **may prevent the creation of + a hybrid vertical coordinate**. While these cases used to "work", this was + never really correct behaviour. (:pull:`3795`) + +#. `@SimonPeatman`_ added attribute ``var_name`` to coordinates created by the + :func:`iris.analysis.trajectory.interpolate` function. This prevents + duplicate coordinate errors in certain circumstances. (:pull:`3718`) + +#. `@bjlittle`_ aligned the :func:`iris.analysis.maths.apply_ufunc` with the + rest of the :mod:`iris.analysis.maths` API by changing its keyword argument + from ``other_cube`` to ``other``. (:pull:`3785`) + +#. `@bjlittle`_ changed the :meth:`iris.analysis.maths.IFunc.__call__` to ignore + any surplus ``other`` keyword argument for a ``data_func`` that requires + **only one** argument. This aligns the behaviour of + :meth:`iris.analysis.maths.IFunc.__call__` with + :func:`~iris.analysis.maths.apply_ufunc`. Previously a ``ValueError`` + exception was raised. (:pull:`3785`) .. _whatsnew 3.0 deprecations: @@ -248,48 +248,48 @@ This document explains the changes made to Iris for this release 🔥 Deprecations =============== -* `@stephenworsley`_ removed the deprecated :class:`iris.Future` flags - ``cell_date_time_objects``, ``netcdf_promote``, ``netcdf_no_unlimited`` and - ``clip_latitudes``. (:pull:`3459`) +#. `@stephenworsley`_ removed the deprecated :class:`iris.Future` flags + ``cell_date_time_objects``, ``netcdf_promote``, ``netcdf_no_unlimited`` and + ``clip_latitudes``. (:pull:`3459`) -* `@stephenworsley`_ changed :attr:`iris.fileformats.pp.PPField.lbproc` to be an - ``int``. The deprecated attributes ``flag1``, ``flag2`` etc. have been - removed from it. (:pull:`3461`) +#. `@stephenworsley`_ changed :attr:`iris.fileformats.pp.PPField.lbproc` to be an + ``int``. The deprecated attributes ``flag1``, ``flag2`` etc. have been + removed from it. (:pull:`3461`) -* `@bjlittle`_ deprecated :func:`~iris.util.as_compatible_shape` in preference - for :class:`~iris.common.resolve.Resolve` e.g., ``Resolve(src, tgt)(tgt.core_data())``. - The :func:`~iris.util.as_compatible_shape` function will be removed in a future - release of Iris. (:pull:`3892`) +#. `@bjlittle`_ deprecated :func:`~iris.util.as_compatible_shape` in preference + for :class:`~iris.common.resolve.Resolve` e.g., ``Resolve(src, tgt)(tgt.core_data())``. + The :func:`~iris.util.as_compatible_shape` function will be removed in a future + release of Iris. (:pull:`3892`) 🔗 Dependencies =============== -* `@stephenworsley`_, `@trexfeathers`_ and `@bjlittle`_ removed ``Python2`` - support, modernising the codebase by switching to exclusive ``Python3`` - support. (:pull:`3513`) +#. `@stephenworsley`_, `@trexfeathers`_ and `@bjlittle`_ removed ``Python2`` + support, modernising the codebase by switching to exclusive ``Python3`` + support. (:pull:`3513`) -* `@bjlittle`_ improved the developer set up process. Configuring Iris and - :ref:`installing_from_source` as a developer with all the required package - dependencies is now easier with our curated conda environment YAML files. - (:pull:`3812`) +#. `@bjlittle`_ improved the developer set up process. Configuring Iris and + :ref:`installing_from_source` as a developer with all the required package + dependencies is now easier with our curated conda environment YAML files. + (:pull:`3812`) -* `@stephenworsley`_ pinned Iris to require `Dask`_ ``>=2.0``. (:pull:`3460`) +#. `@stephenworsley`_ pinned Iris to require `Dask`_ ``>=2.0``. (:pull:`3460`) -* `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require - `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version - of `Matplotlib`_. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ pinned Iris to require + `Cartopy`_ ``>=0.18``, in order to remain compatible with the latest version + of `Matplotlib`_. (:pull:`3762`) -* `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. - Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in - pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer - necessary now that ``Python2`` support has been dropped. (:pull:`3468`) +#. `@bjlittle`_ unpinned Iris to use the latest version of `Matplotlib`_. + Supporting ``Iris`` for both ``Python2`` and ``Python3`` had resulted in + pinning our dependency on `Matplotlib`_ at ``v2.x``. But this is no longer + necessary now that ``Python2`` support has been dropped. (:pull:`3468`) -* `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version - of `Proj`_. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ unpinned Iris to use the latest version + of `Proj`_. (:pull:`3762`) -* `@stephenworsley`_ and `@trexfeathers`_ removed GDAL from the extensions - dependency group. We no longer consider it to be an extension. (:pull:`3762`) +#. `@stephenworsley`_ and `@trexfeathers`_ removed GDAL from the extensions + dependency group. We no longer consider it to be an extension. (:pull:`3762`) .. _whatsnew 3.0 docs: @@ -297,157 +297,160 @@ This document explains the changes made to Iris for this release 📚 Documentation ================ -* `@tkknight`_ moved the - :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` - from the general part of the gallery to oceanography. (:pull:`3761`) +#. `@tkknight`_ moved the + :ref:`sphx_glr_generated_gallery_oceanography_plot_orca_projection.py` + from the general part of the gallery to oceanography. (:pull:`3761`) -* `@tkknight`_ updated documentation to use a modern sphinx theme and be - served from https://scitools-iris.readthedocs.io/en/latest/. (:pull:`3752`) +#. `@tkknight`_ updated documentation to use a modern sphinx theme and be + served from https://scitools-iris.readthedocs.io/en/latest/. (:pull:`3752`) -* `@bjlittle`_ added support for the `black`_ code formatter. This is - now automatically checked on GitHub PRs, replacing the older, unittest-based - ``iris.tests.test_coding_standards.TestCodeFormat``. Black provides automatic - code format correction for most IDEs. See the new developer guide section on - :ref:`code_formatting`. (:pull:`3518`) +#. `@bjlittle`_ added support for the `black`_ code formatter. This is + now automatically checked on GitHub PRs, replacing the older, unittest-based + ``iris.tests.test_coding_standards.TestCodeFormat``. Black provides automatic + code format correction for most IDEs. See the new developer guide section on + :ref:`code_formatting`. (:pull:`3518`) -* `@tkknight`_ and `@trexfeathers`_ refreshed the :ref:`whats_new_contributions` - for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` - what's new page so it appears on the latest documentation at - https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves - :issue:`2104`, :issue:`3451`, :issue:`3818`, :issue:`3837`. Also updated the - :ref:`iris_development_releases_steps` to follow when making a release. - (:pull:`3769`, :pull:`3838`, :pull:`3843`) +#. `@tkknight`_ and `@trexfeathers`_ refreshed the :ref:`whats_new_contributions` + for the :ref:`iris_whatsnew`. This includes always creating the ``latest`` + what's new page so it appears on the latest documentation at + https://scitools-iris.readthedocs.io/en/latest/whatsnew. This resolves + :issue:`2104`, :issue:`3451`, :issue:`3818`, :issue:`3837`. Also updated the + :ref:`iris_development_releases_steps` to follow when making a release. + (:pull:`3769`, :pull:`3838`, :pull:`3843`) -* `@tkknight`_ enabled the PDF creation of the documentation on the - `Read the Docs`_ service. The PDF may be accessed by clicking on the version - at the bottom of the side bar, then selecting ``PDF`` from the ``Downloads`` - section. (:pull:`3765`) +#. `@tkknight`_ enabled the PDF creation of the documentation on the + `Read the Docs`_ service. The PDF may be accessed by clicking on the version + at the bottom of the side bar, then selecting ``PDF`` from the ``Downloads`` + section. (:pull:`3765`) -* `@stephenworsley`_ added a warning to the - :func:`iris.analysis.cartography.project` function regarding its behaviour on - projections with non-rectangular boundaries. (:pull:`3762`) +#. `@stephenworsley`_ added a warning to the + :func:`iris.analysis.cartography.project` function regarding its behaviour on + projections with non-rectangular boundaries. (:pull:`3762`) -* `@stephenworsley`_ added the :ref:`cube_maths_combining_units` section to the - user guide to clarify how ``Units`` are handled during cube arithmetic. - (:pull:`3803`) +#. `@stephenworsley`_ added the :ref:`cube_maths_combining_units` section to the + user guide to clarify how ``Units`` are handled during cube arithmetic. + (:pull:`3803`) -* `@tkknight`_ overhauled the :ref:`developers_guide` including information on - getting involved in becoming a contributor and general structure of the - guide. This resolves :issue:`2170`, :issue:`2331`, :issue:`3453`, - :issue:`314`, :issue:`2902`. (:pull:`3852`) +#. `@tkknight`_ overhauled the :ref:`developers_guide` including information on + getting involved in becoming a contributor and general structure of the + guide. This resolves :issue:`2170`, :issue:`2331`, :issue:`3453`, + :issue:`314`, :issue:`2902`. (:pull:`3852`) -* `@rcomer`_ added argument descriptions to the :class:`~iris.coords.DimCoord` - docstring. (:pull:`3681`) +#. `@rcomer`_ added argument descriptions to the :class:`~iris.coords.DimCoord` + docstring. (:pull:`3681`) -* `@tkknight`_ added two url's to be ignored for the ``make linkcheck``. This - will ensure the Iris github project is not repeatedly hit during the - linkcheck for issues and pull requests as it can result in connection - refused and thus travis-ci_ job failures. For more information on linkcheck, - see :ref:`contributing.documentation.testing`. (:pull:`3873`) +#. `@tkknight`_ added two url's to be ignored for the ``make linkcheck``. This + will ensure the Iris github project is not repeatedly hit during the + linkcheck for issues and pull requests as it can result in connection + refused and thus travis-ci_ job failures. For more information on linkcheck, + see :ref:`contributing.documentation.testing`. (:pull:`3873`) -* `@tkknight`_ enabled the napolean_ package that is used by sphinx_ to cater - for the existing google style docstrings and to also allow for `numpy`_ - docstrings. This resolves :issue:`3841`. (:pull:`3871`) +#. `@tkknight`_ enabled the napolean_ package that is used by sphinx_ to cater + for the existing google style docstrings and to also allow for `numpy`_ + docstrings. This resolves :issue:`3841`. (:pull:`3871`) -* `@tkknight`_ configured ``sphinx-build`` to promote warnings to errors when - building the documentation via ``make html``. This will minimise technical - debt accruing for the documentation. (:pull:`3877`) +#. `@tkknight`_ configured ``sphinx-build`` to promote warnings to errors when + building the documentation via ``make html``. This will minimise technical + debt accruing for the documentation. (:pull:`3877`) -* `@tkknight`_ updated :ref:`installing_iris` to include a reference to - Windows Subsystem for Linux. (:pull:`3885`) +#. `@tkknight`_ updated :ref:`installing_iris` to include a reference to + Windows Subsystem for Linux. (:pull:`3885`) -* `@tkknight`_ updated the :ref:`iris_docs` homepage to include panels so the - links are more visible to users. This uses the sphinx-panels_ extension. - (:pull:`3884`) +#. `@tkknight`_ updated the :ref:`iris_docs` homepage to include panels so the + links are more visible to users. This uses the sphinx-panels_ extension. + (:pull:`3884`) -* `@bjlittle`_ created the :ref:`Further topics ` section and - included documentation for :ref:`metadata`, :ref:`lenient metadata`, and - :ref:`lenient maths`. (:pull:`3890`) +#. `@bjlittle`_ created the :ref:`Further topics ` section and + included documentation for :ref:`metadata`, :ref:`lenient metadata`, and + :ref:`lenient maths`. (:pull:`3890`) -* `@jonseddon`_ updated the CF version of the netCDF saver in the - :ref:`saving_iris_cubes` section and in the equivalent function docstring. - (:pull:`3925`) +#. `@jonseddon`_ updated the CF version of the netCDF saver in the + :ref:`saving_iris_cubes` section and in the equivalent function docstring. + (:pull:`3925`) -* `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. - (:pull:`3940`) +#. `@bjlittle`_ applied `Title Case Capitalization`_ to the documentation. + (:pull:`3940`) 💼 Internal =========== -* `@pp-mo`_ and `@lbdreyer`_ removed all Iris test dependencies on `iris-grib`_ - by transferring all relevant content to the `iris-grib`_ repository. (:pull:`3662`, - :pull:`3663`, :pull:`3664`, :pull:`3665`, :pull:`3666`, :pull:`3669`, - :pull:`3670`, :pull:`3671`, :pull:`3672`, :pull:`3742`, :pull:`3746`) +#. `@pp-mo`_ and `@lbdreyer`_ removed all Iris test dependencies on `iris-grib`_ + by transferring all relevant content to the `iris-grib`_ repository. (:pull:`3662`, + :pull:`3663`, :pull:`3664`, :pull:`3665`, :pull:`3666`, :pull:`3669`, + :pull:`3670`, :pull:`3671`, :pull:`3672`, :pull:`3742`, :pull:`3746`) -* `@lbdreyer`_ and `@pp-mo`_ overhauled the handling of dimensional - metadata to remove duplication. (:pull:`3422`, :pull:`3551`) +#. `@lbdreyer`_ and `@pp-mo`_ overhauled the handling of dimensional + metadata to remove duplication. (:pull:`3422`, :pull:`3551`) -* `@trexfeathers`_ simplified the standard license header for all files, which - removes the need to repeatedly update year numbers in the header. - (:pull:`3489`) +#. `@trexfeathers`_ simplified the standard license header for all files, which + removes the need to repeatedly update year numbers in the header. + (:pull:`3489`) -* `@stephenworsley`_ changed the numerical values in tests involving the - Robinson projection due to improvements made in - `Proj`_. (:pull:`3762`) (see also `Proj#1292`_ and `Proj#2151`_) +#. `@stephenworsley`_ changed the numerical values in tests involving the + Robinson projection due to improvements made in + `Proj`_. (:pull:`3762`) (see also `Proj#1292`_ and `Proj#2151`_) -* `@stephenworsley`_ changed tests to account for more detailed descriptions of - projections in `GDAL`_. (:pull:`3762`) (see also `GDAL#1185`_) +#. `@stephenworsley`_ changed tests to account for more detailed descriptions of + projections in `GDAL`_. (:pull:`3762`) (see also `GDAL#1185`_) -* `@stephenworsley`_ changed tests to account for `GDAL`_ now saving fill values - for data without masked points. (:pull:`3762`) +#. `@stephenworsley`_ changed tests to account for `GDAL`_ now saving fill values + for data without masked points. (:pull:`3762`) -* `@trexfeathers`_ changed every graphics test that includes `Cartopy's coastlines`_ - to account for new adaptive coastline scaling. (:pull:`3762`) - (see also `Cartopy#1105`_) +#. `@trexfeathers`_ changed every graphics test that includes `Cartopy's coastlines`_ + to account for new adaptive coastline scaling. (:pull:`3762`) + (see also `Cartopy#1105`_) -* `@trexfeathers`_ changed graphics tests to account for some new default - grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) +#. `@trexfeathers`_ changed graphics tests to account for some new default + grid-line spacing in `Cartopy`_. (:pull:`3762`) (see also `Cartopy#1117`_) -* `@trexfeathers`_ added additional acceptable graphics test targets to account - for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and - axes borders). (:pull:`3762`) +#. `@trexfeathers`_ added additional acceptable graphics test targets to account + for very minor changes in `Matplotlib`_ version ``3.3`` (colormaps, fonts and + axes borders). (:pull:`3762`) -* `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore - `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. - (:pull:`3846`) +#. `@rcomer`_ corrected the Matplotlib backend in Iris tests to ignore + `matplotlib.rcdefaults`_, instead the tests will **always** use ``agg``. + (:pull:`3846`) -* `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. - (:pull:`3866`) +#. `@bjlittle`_ migrated the `black`_ support from ``19.10b0`` to ``20.8b1``. + (:pull:`3866`) -* `@lbdreyer`_ updated the CF standard name table to the latest version: `v75`_. - (:pull:`3867`) +#. `@lbdreyer`_ updated the CF standard name table to the latest version: `v75`_. + (:pull:`3867`) -* `@bjlittle`_ added :pep:`517` and :pep:`518` support for building and - installing Iris, in particular to handle the `PyKE`_ package dependency. - (:pull:`3812`) +#. `@bjlittle`_ added :pep:`517` and :pep:`518` support for building and + installing Iris, in particular to handle the `PyKE`_ package dependency. + (:pull:`3812`) -* `@bjlittle`_ added metadata support for comparing :attr:`~iris.cube.Cube.attributes` - dictionaries that contain `numpy`_ arrays using `xxHash`_, an extremely fast - non-cryptographic hash algorithm, running at RAM speed limits. +#. `@bjlittle`_ added metadata support for comparing :attr:`~iris.cube.Cube.attributes` + dictionaries that contain `numpy`_ arrays using `xxHash`_, an extremely fast + non-cryptographic hash algorithm, running at RAM speed limits. -* `@bjlittle`_ added the ``iris.tests.assertDictEqual`` method to override - :meth:`unittest.TestCase.assertDictEqual` in order to cope with testing - metadata :attr:`~iris.cube.Cube.attributes` dictionary comparison where - the value of a key may be a `numpy`_ array. (:pull:`3785`) +#. `@bjlittle`_ added the ``iris.tests.assertDictEqual`` method to override + :meth:`unittest.TestCase.assertDictEqual` in order to cope with testing + metadata :attr:`~iris.cube.Cube.attributes` dictionary comparison where + the value of a key may be a `numpy`_ array. (:pull:`3785`) -* `@bjlittle`_ added the :func:`~iris.config.get_logger` function for creating - a generic :class:`logging.Logger` with a :class:`logging.StreamHandler` and - custom :class:`logging.Formatter`. (:pull:`3785`) +#. `@bjlittle`_ added the :func:`~iris.config.get_logger` function for creating + a generic :class:`logging.Logger` with a :class:`logging.StreamHandler` and + custom :class:`logging.Formatter`. (:pull:`3785`) -* `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header - loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) +#. `@owena11`_ identified and optimised a bottleneck in ``FieldsFile`` header + loading due to the use of :func:`numpy.fromfile`. (:pull:`3791`) -* `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). +#. `@znicholls`_ added a test for plotting with the label being taken from the unit's symbol, + see :meth:`~iris.tests.test_quickplot.TestLabels.test_pcolormesh_str_symbol` (:pull:`3902`). -* `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). +#. `@znicholls`_ made :func:`~iris.tests.idiff.step_over_diffs` robust to hyphens (``-``) in + the input path (i.e. the ``result_dir`` argument) (:pull:`3902`). -* `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ support. (:pull:`3928`) +#. `@bjlittle`_ migrated the CIaaS from `travis-ci`_ to `cirrus-ci`_, and removed `stickler-ci`_ + support. (:pull:`3928`) -* `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. - It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to - run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris - with `flake8`_ and `black`_. (:pull:`3928`) +#. `@bjlittle`_ introduced `nox`_ as a common and easy entry-point for test automation. + It can be used both from `cirrus-ci`_ in the cloud, and locally by the developer to + run the Iris tests, the doc-tests, the gallery doc-tests, and lint Iris + with `flake8`_ and `black`_. (:pull:`3928`) .. _Read the Docs: https://scitools-iris.readthedocs.io/en/latest/ .. _Matplotlib: https://matplotlib.org/ diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 6205dc6bfaf..5809b3cf2e2 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -10,58 +10,59 @@ This document explains the changes made to Iris for this release 📢 Announcements ================ -* N/A +#. N/A ✨ Features =========== -* `@pelson`_ and `@trexfeathers`_ enhanced :meth:iris.plot.plot and - :meth:iris.quickplot.plot to automatically place the cube on the x axis if - the primary coordinate being plotted against is a vertical coordinate. E.g. - ``iris.plot.plot(z_cube)`` will produce a z-vs-phenomenon plot, where before - it would have produced a phenomenon-vs-z plot. (:pull:`3906`) +#. `@pelson`_ and `@trexfeathers`_ enhanced :meth:iris.plot.plot and + :meth:iris.quickplot.plot to automatically place the cube on the x axis if + the primary coordinate being plotted against is a vertical coordinate. E.g. + ``iris.plot.plot(z_cube)`` will produce a z-vs-phenomenon plot, where before + it would have produced a phenomenon-vs-z plot. (:pull:`3906`) 🐛 Bugs Fixed ============= -* `@gcaria`_ fixed :meth:`~iris.cube.Cube.cell_measure_dims` to also accept the string name of a :class:`~iris.coords.CellMeasure`. (:pull:`3931`) -* `@gcaria`_ fixed :meth:`~iris.cube.Cube.ancillary_variable_dims` to also accept the string name of a :class:`~iris.coords.AncillaryVariable`. (:pull:`3931`) +#. `@gcaria`_ fixed :meth:`~iris.cube.Cube.cell_measure_dims` to also accept the + string name of a :class:`~iris.coords.CellMeasure`. (:pull:`3931`) +#. `@gcaria`_ fixed :meth:`~iris.cube.Cube.ancillary_variable_dims` to also accept + the string name of a :class:`~iris.coords.AncillaryVariable`. (:pull:`3931`) 💣 Incompatible Changes ======================= -* N/A +#. N/A 🔥 Deprecations =============== -* N/A +#. N/A 🔗 Dependencies =============== -* N/A +#. N/A 📚 Documentation ================ -* `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. - (:pull:`3933`) -* `@MHBalsmeier`_ Described non-conda installation on Debian-based distros. - (:pull:`3958`) +#. `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. + (:pull:`3933`) +#. `@MHBalsmeier`_ Described non-conda installation on Debian-based distros. + (:pull:`3958`) 💼 Internal =========== -* `@rcomer`_ removed an old unused test file. (:pull:`3913`) - +#. `@rcomer`_ removed an old unused test file. (:pull:`3913`) .. _@pelson: https://github.com/pelson diff --git a/docs/iris/src/whatsnew/latest.rst.template b/docs/iris/src/whatsnew/latest.rst.template index 67518e539a5..06c04f264f4 100644 --- a/docs/iris/src/whatsnew/latest.rst.template +++ b/docs/iris/src/whatsnew/latest.rst.template @@ -10,46 +10,46 @@ This document explains the changes made to Iris for this release 📢 Announcements ================ -* N/A +#. N/A ✨ Features =========== -* N/A +#. N/A 🐛 Bugs Fixed ============= -* N/A +#. N/A 💣 Incompatible Changes ======================= -* N/A +#. N/A 🔥 Deprecations =============== -* N/A +#. N/A 🔗 Dependencies =============== -* N/A +#. N/A 📚 Documentation ================ -* N/A +#. N/A 💼 Internal =========== -* N/A +#. N/A From 28b494d963f447d51cd9e85a28bd85a61cb254cf Mon Sep 17 00:00:00 2001 From: Bill Little Date: Sun, 31 Jan 2021 18:53:02 +0000 Subject: [PATCH 12/23] Docs whatsnew add dropdowns to the template (#3969) * add release highlights dropdown to latest and template * add the patches dropdown to latest template * make the patch release pulldown v3 generic --- docs/iris/src/whatsnew/latest.rst | 17 +++++++++++ docs/iris/src/whatsnew/latest.rst.template | 34 +++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 5809b3cf2e2..f33e546dc37 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -7,6 +7,21 @@ This document explains the changes made to Iris for this release (:doc:`View all changes `.) +.. dropdown:: :opticon:`report` Release Highlights + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The highlights for this major/minor release of Iris include: + + * N/A + + And finally, get in touch with us on `GitHub`_ if you have any issues or + feature requests for improving Iris. Enjoy! + + 📢 Announcements ================ @@ -65,6 +80,8 @@ This document explains the changes made to Iris for this release #. `@rcomer`_ removed an old unused test file. (:pull:`3913`) + +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose .. _@pelson: https://github.com/pelson .. _@trexfeathers: https://github.com/trexfeathers .. _@gcaria: https://github.com/gcaria diff --git a/docs/iris/src/whatsnew/latest.rst.template b/docs/iris/src/whatsnew/latest.rst.template index 06c04f264f4..0dd7cc788ba 100644 --- a/docs/iris/src/whatsnew/latest.rst.template +++ b/docs/iris/src/whatsnew/latest.rst.template @@ -7,6 +7,33 @@ This document explains the changes made to Iris for this release (:doc:`View all changes `.) +.. dropdown:: :opticon:`alert` v3.X.X Patches + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The patches in this release of Iris include: + + #. N/A + + +.. dropdown:: :opticon:`report` Release Highlights + :container: + shadow + :title: text-primary text-center font-weight-bold + :body: bg-light + :animate: fade-in + :open: + + The highlights for this major/minor release of Iris include: + + * N/A + + And finally, get in touch with us on `GitHub`_ if you have any issues or + feature requests for improving Iris. Enjoy! + + 📢 Announcements ================ @@ -52,4 +79,9 @@ This document explains the changes made to Iris for this release 💼 Internal =========== -#. N/A +* N/A + + + + +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose From 15bcd700807c747f2a856deb911b2abeed3f5ff5 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 1 Feb 2021 11:50:12 +0000 Subject: [PATCH 13/23] reorganise docs common links + add core devs (#3972) * reorganise docs common links + add core devs * add common links comment --- docs/iris/src/common_links.inc | 66 ++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/docs/iris/src/common_links.inc b/docs/iris/src/common_links.inc index 3941bfaff23..050752a483a 100644 --- a/docs/iris/src/common_links.inc +++ b/docs/iris/src/common_links.inc @@ -1,27 +1,57 @@ -.. _SciTools: https://github.com/SciTools +.. comment + Common resources in alphabetical order: + +.. _.cirrus.yml: https://github.com/SciTools/iris/blob/master/.cirrus.yml +.. _.flake8.yml: https://github.com/SciTools/iris/blob/master/.flake8 +.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris +.. _conda: https://docs.conda.io/en/latest/ +.. _contributor: https://github.com/SciTools/scitools.org.uk/blob/master/contributors.json +.. _core developers: https://github.com/SciTools/scitools.org.uk/blob/master/contributors.json +.. _generating sss keys for GitHub: https://docs.github.com/en/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account +.. _GitHub Help Documentation: https://docs.github.com/en/github .. _Iris: https://github.com/SciTools/iris .. _Iris GitHub: https://github.com/SciTools/iris .. _iris mailing list: https://groups.google.com/forum/#!forum/scitools-iris +.. _iris-sample-data: https://github.com/SciTools/iris-sample-data +.. _iris-test-data: https://github.com/SciTools/iris-test-data .. _issue: https://github.com/SciTools/iris/issues .. _issues: https://github.com/SciTools/iris/issues +.. _legacy documentation: https://scitools.org.uk/iris/docs/v2.4.0/ +.. _matplotlib: https://matplotlib.org/ +.. _napolean: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/sphinxcontrib.napoleon.html +.. _New Issue: https://github.com/scitools/iris/issues/new/choose .. _pull request: https://github.com/SciTools/iris/pulls .. _pull requests: https://github.com/SciTools/iris/pulls -.. _contributor: https://github.com/SciTools/scitools.org.uk/blob/master/contributors.json -.. _core developers: https://github.com/SciTools/scitools.org.uk/blob/master/contributors.json -.. _iris-test-data: https://github.com/SciTools/iris-test-data -.. _iris-sample-data: https://github.com/SciTools/iris-sample-data -.. _test-iris-imagehash: https://github.com/SciTools/test-iris-imagehash .. _readthedocs.yml: https://github.com/SciTools/iris/blob/master/requirements/ci/readthedocs.yml -.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris -.. _.cirrus.yml: https://github.com/SciTools/iris/blob/master/.cirrus.yml -.. _.flake8.yml: https://github.com/SciTools/iris/blob/master/.flake8 -.. _GitHub Help Documentation: https://docs.github.com/en/github -.. _using git: https://docs.github.com/en/github/using-git -.. _generating sss keys for GitHub: https://docs.github.com/en/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account -.. _New Issue: https://github.com/scitools/iris/issues/new/choose -.. _matplotlib: https://matplotlib.org/ -.. _conda: https://docs.conda.io/en/latest/ +.. _SciTools: https://github.com/SciTools .. _sphinx: https://www.sphinx-doc.org/en/master/ -.. _napolean: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/sphinxcontrib.napoleon.html -.. _legacy documentation: https://scitools.org.uk/iris/docs/v2.4.0/ -.. _cirrus-ci: https://cirrus-ci.com/github/SciTools/iris +.. _test-iris-imagehash: https://github.com/SciTools/test-iris-imagehash +.. _using git: https://docs.github.com/en/github/using-git + + +.. comment + Core developers (@github names) in alphabetical order: + +.. _@abooton: https://github.com/abooton +.. _@alastair-gemmell: https://github.com/alastair-gemmell +.. _@ajdawson: https://github.com/ajdawson +.. _@bjlittle: https://github.com/bjlittle +.. _@bouweandela: https://github.com/bouweandela +.. _@corinnebosley: https://github.com/corinnebosley +.. _@cpelley: https://github.com/cpelley +.. _@djkirkham: https://github.com/djkirkham +.. _@DPeterK: https://github.com/DPeterK +.. _@esc24: https://github.com/esc24 +.. _@jonseddon: https://github.com/jonseddon +.. _@jvegasbsc: https://github.com/jvegasbsc +.. _@lbdreyer: https://github.com/lbdreyer +.. _@marqh: https://github.com/marqh +.. _@pelson: https://github.com/pelson +.. _@pp-mo: https://github.com/pp-mo +.. _@QuLogic: https://github.com/QuLogic +.. _@rcomer: https://github.com/rcomer +.. _@rhattersley: https://github.com/rhattersley +.. _@stephenworsley: https://github.com/stephenworsley +.. _@tkknight: https://github.com/tkknight +.. _@trexfeathers: https://github.com/trexfeathers +.. _@zklaus: https://github.com/zklaus From 1e8ccdfc037f412b4ddc4b19d1fda75a13fb223b Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 1 Feb 2021 12:01:08 +0000 Subject: [PATCH 14/23] document that iris.coords.Coord is an ABC (#3971) * document that iris.coords.Coord is an ABC * use rst comment directive instead of only directive --- docs/iris/src/whatsnew/latest.rst | 28 +++++--- docs/iris/src/whatsnew/latest.rst.template | 9 ++- lib/iris/coords.py | 76 +++++++++++++++++----- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index f33e546dc37..0b1b8db1b64 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -43,6 +43,7 @@ This document explains the changes made to Iris for this release #. `@gcaria`_ fixed :meth:`~iris.cube.Cube.cell_measure_dims` to also accept the string name of a :class:`~iris.coords.CellMeasure`. (:pull:`3931`) + #. `@gcaria`_ fixed :meth:`~iris.cube.Cube.ancillary_variable_dims` to also accept the string name of a :class:`~iris.coords.AncillaryVariable`. (:pull:`3931`) @@ -68,10 +69,12 @@ This document explains the changes made to Iris for this release 📚 Documentation ================ -#. `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. - (:pull:`3933`) -#. `@MHBalsmeier`_ Described non-conda installation on Debian-based distros. - (:pull:`3958`) +#. `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. (:pull:`3933`) + +#. `@MHBalsmeier`_ described non-conda installation on Debian-based distros. (:pull:`3958`) + +#. `@bjlittle`_ clarified in the doc-string that :class:`~iris.coords.Coord` is now an `abstract base class`_ of + coordinates since ``v3.0.0``, and it is **not** possible to create an instance of it. (:pull:`3971`) 💼 Internal @@ -80,10 +83,19 @@ This document explains the changes made to Iris for this release #. `@rcomer`_ removed an old unused test file. (:pull:`3913`) +.. comment + What's new author names (@github name) in alphabetical order: -.. _GitHub: https://github.com/SciTools/iris/issues/new/choose -.. _@pelson: https://github.com/pelson -.. _@trexfeathers: https://github.com/trexfeathers +.. _@bjlittle: https://github.com/bjlittle .. _@gcaria: https://github.com/gcaria -.. _@rcomer: https://github.com/rcomer .. _@MHBalsmeier: https://github.com/MHBalsmeier +.. _@pelson: https://github.com/pelson +.. _@rcomer: https://github.com/rcomer +.. _@trexfeathers: https://github.com/trexfeathers + + +.. comment + What's new resources in alphabetical order: + +.. _abstract base class: https://docs.python.org/3/library/abc.html +.. _GitHub: https://github.com/SciTools/iris/issues/new/choose diff --git a/docs/iris/src/whatsnew/latest.rst.template b/docs/iris/src/whatsnew/latest.rst.template index 0dd7cc788ba..75a1f8cd762 100644 --- a/docs/iris/src/whatsnew/latest.rst.template +++ b/docs/iris/src/whatsnew/latest.rst.template @@ -79,9 +79,16 @@ This document explains the changes made to Iris for this release 💼 Internal =========== -* N/A +#. N/A + + +.. comment + What's new author names (@github name) in alphabetical order: + +.. comment + What's new resources in alphabetical order: .. _GitHub: https://github.com/SciTools/iris/issues/new/choose diff --git a/lib/iris/coords.py b/lib/iris/coords.py index 76ca83cd968..cfeb24cdcb3 100644 --- a/lib/iris/coords.py +++ b/lib/iris/coords.py @@ -12,7 +12,6 @@ from collections import namedtuple from collections.abc import Iterator import copy -from functools import wraps from itertools import chain, zip_longest import operator import warnings @@ -1272,7 +1271,7 @@ def contains_point(self, point): class Coord(_DimensionalMetadata): """ - Superclass for coordinates. + Abstract base class for coordinates. """ @@ -1291,7 +1290,7 @@ def __init__( ): """ - Constructs a single coordinate. + Coordinate abstract base class. As of ``v3.0.0`` you **cannot** create an instance of :class:`Coord`. Args: @@ -1313,17 +1312,17 @@ def __init__( * bounds An array of values describing the bounds of each cell. Given n bounds for each cell, the shape of the bounds array should be - points.shape + (n,). For example, a 1d coordinate with 100 points + points.shape + (n,). For example, a 1D coordinate with 100 points and two bounds per cell would have a bounds array of shape (100, 2) Note if the data is a climatology, `climatological` should be set. * attributes - A dictionary containing other cf and user-defined attributes. + A dictionary containing other CF and user-defined attributes. * coord_system A :class:`~iris.coord_systems.CoordSystem` representing the coordinate system of the coordinate, - e.g. a :class:`~iris.coord_systems.GeogCS` for a longitude Coord. + e.g., a :class:`~iris.coord_systems.GeogCS` for a longitude coordinate. * climatological (bool): When True: the coordinate is a NetCDF climatological time axis. When True: saving in NetCDF will give the coordinate variable a @@ -2250,7 +2249,8 @@ def _xml_id_extra(self, unique_value): class DimCoord(Coord): """ - A coordinate that is 1D, numeric, and strictly monotonic. + A coordinate that is 1D, and numeric, with values that have a strict monotonic ordering. Missing values are not + permitted in a :class:`DimCoord`. """ @@ -2275,7 +2275,7 @@ def from_regular( optionally bounds. The majority of the arguments are defined as for - :meth:`Coord.__init__`, but those which differ are defined below. + :class:`Coord`, but those which differ are defined below. Args: @@ -2336,8 +2336,9 @@ def __init__( climatological=False, ): """ - Create a 1D, numeric, and strictly monotonic :class:`Coord` with - read-only points and bounds. + Create a 1D, numeric, and strictly monotonic coordinate with **immutable** points and bounds. + + Missing values are not permitted. Args: @@ -2369,11 +2370,11 @@ def __init__( Note if the data is a climatology, `climatological` should be set. * attributes: - A dictionary containing other cf and user-defined attributes. + A dictionary containing other CF and user-defined attributes. * coord_system: A :class:`~iris.coord_systems.CoordSystem` representing the coordinate system of the coordinate, - e.g. a :class:`~iris.coord_systems.GeogCS` for a longitude Coord. + e.g., a :class:`~iris.coord_systems.GeogCS` for a longitude coordinate. * circular (bool): Whether the coordinate wraps by the :attr:`~iris.coords.DimCoord.units.modulus` i.e., the longitude coordinate wraps around the full great circle. @@ -2624,15 +2625,54 @@ class AuxCoord(Coord): """ A CF auxiliary coordinate. - .. note:: - - There are currently no specific properties of :class:`AuxCoord`, - everything is inherited from :class:`Coord`. - """ - @wraps(Coord.__init__, assigned=("__doc__",), updated=()) def __init__(self, *args, **kwargs): + """ + Create a coordinate with **mutable** points and bounds. + + Args: + + * points: + The values (or value in the case of a scalar coordinate) for each + cell of the coordinate. + + Kwargs: + + * standard_name: + CF standard name of the coordinate. + * long_name: + Descriptive name of the coordinate. + * var_name: + The netCDF variable name for the coordinate. + * units + The :class:`~cf_units.Unit` of the coordinate's values. + Can be a string, which will be converted to a Unit object. + * bounds + An array of values describing the bounds of each cell. Given n + bounds for each cell, the shape of the bounds array should be + points.shape + (n,). For example, a 1D coordinate with 100 points + and two bounds per cell would have a bounds array of shape + (100, 2) + Note if the data is a climatology, `climatological` + should be set. + * attributes + A dictionary containing other CF and user-defined attributes. + * coord_system + A :class:`~iris.coord_systems.CoordSystem` representing the + coordinate system of the coordinate, + e.g., a :class:`~iris.coord_systems.GeogCS` for a longitude coordinate. + * climatological (bool): + When True: the coordinate is a NetCDF climatological time axis. + When True: saving in NetCDF will give the coordinate variable a + 'climatology' attribute and will create a boundary variable called + '_climatology' in place of a standard bounds + attribute and bounds variable. + Will set to True when a climatological time axis is loaded + from NetCDF. + Always False if no bounds exist. + + """ super().__init__(*args, **kwargs) # Logically, :class:`Coord` is an abstract class and all actual coords must From 636b97b58f21bbc4d91fe6796a82a1a03e1b5b45 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 1 Feb 2021 13:58:19 +0000 Subject: [PATCH 15/23] remove explicit URLs for core dev names from latest.rst (#3973) --- docs/iris/src/whatsnew/latest.rst | 9 +++------ docs/iris/src/whatsnew/latest.rst.template | 5 +++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 0b1b8db1b64..3cdf5fe6914 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -84,18 +84,15 @@ This document explains the changes made to Iris for this release .. comment - What's new author names (@github name) in alphabetical order: + Whatsnew author names (@github name) in alphabetical order. Note that, + core dev names are automatically included by the common_links.inc: -.. _@bjlittle: https://github.com/bjlittle .. _@gcaria: https://github.com/gcaria .. _@MHBalsmeier: https://github.com/MHBalsmeier -.. _@pelson: https://github.com/pelson -.. _@rcomer: https://github.com/rcomer -.. _@trexfeathers: https://github.com/trexfeathers .. comment - What's new resources in alphabetical order: + Whatsnew resources in alphabetical order: .. _abstract base class: https://docs.python.org/3/library/abc.html .. _GitHub: https://github.com/SciTools/iris/issues/new/choose diff --git a/docs/iris/src/whatsnew/latest.rst.template b/docs/iris/src/whatsnew/latest.rst.template index 75a1f8cd762..0992a5c9bc3 100644 --- a/docs/iris/src/whatsnew/latest.rst.template +++ b/docs/iris/src/whatsnew/latest.rst.template @@ -83,12 +83,13 @@ This document explains the changes made to Iris for this release .. comment - What's new author names (@github name) in alphabetical order: + Whatsnew author names (@github name) in alphabetical order. Note that, + core dev names are automatically included by the common_links.inc: .. comment - What's new resources in alphabetical order: + Whatsnew resources in alphabetical order: .. _GitHub: https://github.com/SciTools/iris/issues/new/choose From 79d636ebc23fc610b8c477e7afaf604c0fc808f9 Mon Sep 17 00:00:00 2001 From: James Penn Date: Wed, 3 Feb 2021 14:55:02 +0000 Subject: [PATCH 16/23] Fix test_incompatible_dimensions test (#3977) * test_incompatible_dimensions used a ragged array for the test, which has been deprecated in numpy, and now fails if dtype is anything other than object. This test appears to be checking that the addition of a [2x4] masked array to a [2x3] masked cube should raise a ValueError. This commit fixes the creation of `data3` object to be a [2x4] non-ragged array. * Added entry to what's new * Added name to core developer list :) * Update latest.rst Fixed space in PR macro call --- docs/iris/src/common_links.inc | 1 + docs/iris/src/whatsnew/latest.rst | 1 + lib/iris/tests/test_basic_maths.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/iris/src/common_links.inc b/docs/iris/src/common_links.inc index 050752a483a..9f6a57f5294 100644 --- a/docs/iris/src/common_links.inc +++ b/docs/iris/src/common_links.inc @@ -42,6 +42,7 @@ .. _@djkirkham: https://github.com/djkirkham .. _@DPeterK: https://github.com/DPeterK .. _@esc24: https://github.com/esc24 +.. _@jamesp: https://github.com/jamesp .. _@jonseddon: https://github.com/jonseddon .. _@jvegasbsc: https://github.com/jvegasbsc .. _@lbdreyer: https://github.com/lbdreyer diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/iris/src/whatsnew/latest.rst index 3cdf5fe6914..618eeb10d61 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/iris/src/whatsnew/latest.rst @@ -82,6 +82,7 @@ This document explains the changes made to Iris for this release #. `@rcomer`_ removed an old unused test file. (:pull:`3913`) +#. `@jamesp`_ updated a test to the latest numpy version (:pull:`3977`) .. comment Whatsnew author names (@github name) in alphabetical order. Note that, diff --git a/lib/iris/tests/test_basic_maths.py b/lib/iris/tests/test_basic_maths.py index 4b3cde95e4f..c4d7b51a065 100644 --- a/lib/iris/tests/test_basic_maths.py +++ b/lib/iris/tests/test_basic_maths.py @@ -853,7 +853,7 @@ def setUp(self): def test_incompatible_dimensions(self): data3 = ma.MaskedArray( - [[3, 3, 3, 4], [2, 2, 2]], mask=[[0, 1, 0, 0], [0, 1, 1]] + [[3, 3, 3, 4], [2, 2, 2, 2]], mask=[[0, 1, 0, 0], [0, 1, 1, 1]] ) with self.assertRaises(ValueError): # Incompatible dimensions. From ea493759b150dbb304b0f0b1ae8a099307766926 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Wed, 3 Feb 2021 21:37:24 +0000 Subject: [PATCH 17/23] moved docs dir and updated references to it (#3975) * moved docs dir and updated references to it * added whatsnew * updated more docs directory references * updated docs dir references --- .cirrus.yml | 8 +++---- .flake8 | 2 +- .gitignore | 6 +++--- .readthedocs.yml | 2 +- CHANGES | 2 +- MANIFEST.in | 2 +- README.md | 2 +- docs/{iris => }/Makefile | 0 docs/{iris => }/gallery_code/README.rst | 0 .../gallery_code/general/README.rst | 0 .../general/plot_SOI_filtering.py | 0 .../general/plot_anomaly_log_colouring.py | 0 .../gallery_code/general/plot_coriolis.py | 0 .../general/plot_cross_section.py | 0 .../general/plot_custom_aggregation.py | 0 .../general/plot_custom_file_loading.py | 0 .../gallery_code/general/plot_global_map.py | 0 .../gallery_code/general/plot_inset.py | 0 .../general/plot_lineplot_with_legend.py | 0 .../gallery_code/general/plot_polar_stereo.py | 0 .../general/plot_polynomial_fit.py | 0 .../plot_projections_and_annotations.py | 0 .../general/plot_rotated_pole_mapping.py | 0 .../gallery_code/meteorology/README.rst | 0 .../gallery_code/meteorology/plot_COP_1d.py | 0 .../gallery_code/meteorology/plot_COP_maps.py | 0 .../gallery_code/meteorology/plot_TEC.py | 0 .../meteorology/plot_deriving_phenomena.py | 0 .../meteorology/plot_hovmoller.py | 0 .../meteorology/plot_lagged_ensemble.py | 0 .../meteorology/plot_wind_speed.py | 0 .../gallery_code/oceanography/README.rst | 0 .../oceanography/plot_atlantic_profiles.py | 0 .../oceanography/plot_load_nemo.py | 0 .../oceanography/plot_orca_projection.py | 0 docs/{iris => }/gallery_tests/__init__.py | 0 .../gallery_tests/gallerytest_util.py | 0 .../gallery_tests/test_plot_COP_1d.py | 0 .../gallery_tests/test_plot_COP_maps.py | 0 .../gallery_tests/test_plot_SOI_filtering.py | 0 .../{iris => }/gallery_tests/test_plot_TEC.py | 0 .../test_plot_anomaly_log_colouring.py | 0 .../test_plot_atlantic_profiles.py | 0 .../gallery_tests/test_plot_coriolis.py | 0 .../gallery_tests/test_plot_cross_section.py | 0 .../test_plot_custom_aggregation.py | 0 .../test_plot_custom_file_loading.py | 0 .../test_plot_deriving_phenomena.py | 0 .../gallery_tests/test_plot_global_map.py | 0 .../gallery_tests/test_plot_hovmoller.py | 0 .../gallery_tests/test_plot_inset.py | 0 .../test_plot_lagged_ensemble.py | 0 .../test_plot_lineplot_with_legend.py | 0 .../gallery_tests/test_plot_load_nemo.py | 0 .../test_plot_orca_projection.py | 0 .../gallery_tests/test_plot_polar_stereo.py | 0 .../gallery_tests/test_plot_polynomial_fit.py | 0 .../test_plot_projections_and_annotations.py | 0 .../test_plot_rotated_pole_mapping.py | 0 .../gallery_tests/test_plot_wind_speed.py | 0 docs/{iris => }/src/IEP/IEP001.adoc | 0 docs/{iris => }/src/Makefile | 0 .../src/_static/Iris7_1_trim_100.png | Bin .../src/_static/Iris7_1_trim_full.png | Bin docs/{iris => }/src/_static/favicon.ico | Bin .../src/_static/iris-logo-title.png | Bin .../src/_static/iris-logo-title.svg | 0 .../{iris => }/src/_static/theme_override.css | 0 docs/{iris => }/src/_templates/layout.html | 0 docs/{iris => }/src/common_links.inc | 0 docs/{iris => }/src/conf.py | 0 docs/{iris => }/src/copyright.rst | 0 .../src/developers_guide/ci_checks.png | Bin .../developers_guide/contributing_changes.rst | 0 .../contributing_ci_tests.rst | 0 .../contributing_code_formatting.rst | 0 .../contributing_codebase_index.rst | 0 .../contributing_deprecations.rst | 0 .../contributing_documentation.rst | 20 +++++++++--------- .../contributing_getting_involved.rst | 0 .../contributing_graphics_tests.rst | 2 +- .../contributing_pull_request_checklist.rst | 0 .../contributing_running_tests.rst | 0 .../developers_guide/contributing_testing.rst | 0 .../contributing_testing_index.rst | 0 .../developers_guide/documenting/__init__.py | 0 .../documenting/docstrings.rst | 0 .../documenting/docstrings_attribute.py | 0 .../documenting/docstrings_sample_routine.py | 0 .../documenting/rest_guide.rst | 0 .../documenting/whats_new_contributions.rst | 2 +- .../src/developers_guide/gitwash/LICENSE | 0 .../gitwash/branch_dropdown.png | Bin .../gitwash/configure_git.rst | 0 .../gitwash/development_workflow.rst | 0 .../src/developers_guide/gitwash/forking.rst | 0 .../gitwash/forking_button.png | Bin .../developers_guide/gitwash/git_intro.rst | 0 .../developers_guide/gitwash/git_links.inc | 0 .../src/developers_guide/gitwash/index.rst | 0 .../src/developers_guide/gitwash/links.inc | 0 .../developers_guide/gitwash/pull_button.png | Bin .../developers_guide/gitwash/set_up_fork.rst | 0 .../src/developers_guide/release.rst | 12 +++++------ docs/{iris => }/src/further_topics/index.rst | 0 .../src/further_topics/lenient_maths.rst | 0 .../src/further_topics/lenient_metadata.rst | 0 .../src/further_topics/metadata.rst | 0 docs/{iris => }/src/index.rst | 0 docs/{iris => }/src/installing.rst | 0 docs/{iris => }/src/spelling_allow.txt | 0 .../src/sphinxext/custom_class_autodoc.py | 0 .../src/sphinxext/custom_data_autodoc.py | 0 .../src/sphinxext/generate_package_rst.py | 0 .../src/techpapers/change_management.rst | 0 docs/{iris => }/src/techpapers/index.rst | 0 .../src/techpapers/missing_data_handling.rst | 0 .../src/techpapers/um_files_loading.rst | 0 .../src/userguide/change_management_goals.txt | 0 docs/{iris => }/src/userguide/citation.rst | 0 .../src/userguide/code_maintenance.rst | 0 docs/{iris => }/src/userguide/concat.png | Bin docs/{iris => }/src/userguide/concat.svg | 0 .../{iris => }/src/userguide/cube_diagram.dia | Bin .../{iris => }/src/userguide/cube_diagram.png | Bin docs/{iris => }/src/userguide/cube_maths.rst | 0 .../src/userguide/cube_statistics.rst | 0 docs/{iris => }/src/userguide/index.rst | 0 .../interpolation_and_regridding.rst | 0 docs/{iris => }/src/userguide/iris_cubes.rst | 0 .../src/userguide/loading_iris_cubes.rst | 0 docs/{iris => }/src/userguide/merge.png | Bin docs/{iris => }/src/userguide/merge.svg | 0 .../src/userguide/merge_and_concat.png | Bin .../src/userguide/merge_and_concat.rst | 0 .../src/userguide/merge_and_concat.svg | 0 docs/{iris => }/src/userguide/multi_array.png | Bin docs/{iris => }/src/userguide/multi_array.svg | 0 .../src/userguide/multi_array_to_cube.png | Bin .../src/userguide/multi_array_to_cube.svg | 0 .../src/userguide/navigating_a_cube.rst | 0 .../src/userguide/plotting_a_cube.rst | 0 .../plotting_examples/1d_quickplot_simple.py | 0 .../userguide/plotting_examples/1d_simple.py | 0 .../plotting_examples/1d_with_legend.py | 0 .../src/userguide/plotting_examples/brewer.py | 0 .../plotting_examples/cube_blockplot.py | 0 .../cube_brewer_cite_contourf.py | 0 .../plotting_examples/cube_brewer_contourf.py | 0 .../plotting_examples/cube_contour.py | 0 .../plotting_examples/cube_contourf.py | 0 .../src/userguide/real_and_lazy_data.rst | 0 .../regridding_plots/interpolate_column.py | 0 .../regridding_plots/regridded_to_global.py | 0 .../regridded_to_global_area_weighted.py | 0 .../regridding_plots/regridded_to_rotated.py | 0 .../regridding_plots/regridding_plot.py | 0 .../src/userguide/saving_iris_cubes.rst | 0 .../src/userguide/subsetting_a_cube.rst | 0 docs/{iris => }/src/whatsnew/1.0.rst | 0 docs/{iris => }/src/whatsnew/1.1.rst | 0 docs/{iris => }/src/whatsnew/1.10.rst | 0 docs/{iris => }/src/whatsnew/1.11.rst | 0 docs/{iris => }/src/whatsnew/1.12.rst | 0 docs/{iris => }/src/whatsnew/1.13.rst | 0 docs/{iris => }/src/whatsnew/1.2.rst | 0 docs/{iris => }/src/whatsnew/1.3.rst | 0 docs/{iris => }/src/whatsnew/1.4.rst | 0 docs/{iris => }/src/whatsnew/1.5.rst | 0 docs/{iris => }/src/whatsnew/1.6.rst | 0 docs/{iris => }/src/whatsnew/1.7.rst | 0 docs/{iris => }/src/whatsnew/1.8.rst | 0 docs/{iris => }/src/whatsnew/1.9.rst | 0 docs/{iris => }/src/whatsnew/2.0.rst | 0 docs/{iris => }/src/whatsnew/2.1.rst | 0 docs/{iris => }/src/whatsnew/2.2.rst | 0 docs/{iris => }/src/whatsnew/2.3.rst | 2 +- docs/{iris => }/src/whatsnew/2.4.rst | 0 docs/{iris => }/src/whatsnew/3.0.1.rst | 0 docs/{iris => }/src/whatsnew/3.0.rst | 0 .../src/whatsnew/images/notebook_repr.png | Bin .../src/whatsnew/images/transverse_merc.png | Bin docs/{iris => }/src/whatsnew/index.rst | 0 docs/{iris => }/src/whatsnew/latest.rst | 15 +++++++++---- .../src/whatsnew/latest.rst.template | 0 lib/iris/tests/test_coding_standards.py | 11 +++++----- noxfile.py | 4 ++-- 187 files changed, 49 insertions(+), 43 deletions(-) rename docs/{iris => }/Makefile (100%) rename docs/{iris => }/gallery_code/README.rst (100%) rename docs/{iris => }/gallery_code/general/README.rst (100%) rename docs/{iris => }/gallery_code/general/plot_SOI_filtering.py (100%) rename docs/{iris => }/gallery_code/general/plot_anomaly_log_colouring.py (100%) rename docs/{iris => }/gallery_code/general/plot_coriolis.py (100%) rename docs/{iris => }/gallery_code/general/plot_cross_section.py (100%) rename docs/{iris => }/gallery_code/general/plot_custom_aggregation.py (100%) rename docs/{iris => }/gallery_code/general/plot_custom_file_loading.py (100%) rename docs/{iris => }/gallery_code/general/plot_global_map.py (100%) rename docs/{iris => }/gallery_code/general/plot_inset.py (100%) rename docs/{iris => }/gallery_code/general/plot_lineplot_with_legend.py (100%) rename docs/{iris => }/gallery_code/general/plot_polar_stereo.py (100%) rename docs/{iris => }/gallery_code/general/plot_polynomial_fit.py (100%) rename docs/{iris => }/gallery_code/general/plot_projections_and_annotations.py (100%) rename docs/{iris => }/gallery_code/general/plot_rotated_pole_mapping.py (100%) rename docs/{iris => }/gallery_code/meteorology/README.rst (100%) rename docs/{iris => }/gallery_code/meteorology/plot_COP_1d.py (100%) rename docs/{iris => }/gallery_code/meteorology/plot_COP_maps.py (100%) rename docs/{iris => }/gallery_code/meteorology/plot_TEC.py (100%) rename docs/{iris => }/gallery_code/meteorology/plot_deriving_phenomena.py (100%) rename docs/{iris => }/gallery_code/meteorology/plot_hovmoller.py (100%) rename docs/{iris => }/gallery_code/meteorology/plot_lagged_ensemble.py (100%) rename docs/{iris => }/gallery_code/meteorology/plot_wind_speed.py (100%) rename docs/{iris => }/gallery_code/oceanography/README.rst (100%) rename docs/{iris => }/gallery_code/oceanography/plot_atlantic_profiles.py (100%) rename docs/{iris => }/gallery_code/oceanography/plot_load_nemo.py (100%) rename docs/{iris => }/gallery_code/oceanography/plot_orca_projection.py (100%) rename docs/{iris => }/gallery_tests/__init__.py (100%) rename docs/{iris => }/gallery_tests/gallerytest_util.py (100%) rename docs/{iris => }/gallery_tests/test_plot_COP_1d.py (100%) rename docs/{iris => }/gallery_tests/test_plot_COP_maps.py (100%) rename docs/{iris => }/gallery_tests/test_plot_SOI_filtering.py (100%) rename docs/{iris => }/gallery_tests/test_plot_TEC.py (100%) rename docs/{iris => }/gallery_tests/test_plot_anomaly_log_colouring.py (100%) rename docs/{iris => }/gallery_tests/test_plot_atlantic_profiles.py (100%) rename docs/{iris => }/gallery_tests/test_plot_coriolis.py (100%) rename docs/{iris => }/gallery_tests/test_plot_cross_section.py (100%) rename docs/{iris => }/gallery_tests/test_plot_custom_aggregation.py (100%) rename docs/{iris => }/gallery_tests/test_plot_custom_file_loading.py (100%) rename docs/{iris => }/gallery_tests/test_plot_deriving_phenomena.py (100%) rename docs/{iris => }/gallery_tests/test_plot_global_map.py (100%) rename docs/{iris => }/gallery_tests/test_plot_hovmoller.py (100%) rename docs/{iris => }/gallery_tests/test_plot_inset.py (100%) rename docs/{iris => }/gallery_tests/test_plot_lagged_ensemble.py (100%) rename docs/{iris => }/gallery_tests/test_plot_lineplot_with_legend.py (100%) rename docs/{iris => }/gallery_tests/test_plot_load_nemo.py (100%) rename docs/{iris => }/gallery_tests/test_plot_orca_projection.py (100%) rename docs/{iris => }/gallery_tests/test_plot_polar_stereo.py (100%) rename docs/{iris => }/gallery_tests/test_plot_polynomial_fit.py (100%) rename docs/{iris => }/gallery_tests/test_plot_projections_and_annotations.py (100%) rename docs/{iris => }/gallery_tests/test_plot_rotated_pole_mapping.py (100%) rename docs/{iris => }/gallery_tests/test_plot_wind_speed.py (100%) rename docs/{iris => }/src/IEP/IEP001.adoc (100%) rename docs/{iris => }/src/Makefile (100%) rename docs/{iris => }/src/_static/Iris7_1_trim_100.png (100%) rename docs/{iris => }/src/_static/Iris7_1_trim_full.png (100%) rename docs/{iris => }/src/_static/favicon.ico (100%) rename docs/{iris => }/src/_static/iris-logo-title.png (100%) rename docs/{iris => }/src/_static/iris-logo-title.svg (100%) rename docs/{iris => }/src/_static/theme_override.css (100%) rename docs/{iris => }/src/_templates/layout.html (100%) rename docs/{iris => }/src/common_links.inc (100%) rename docs/{iris => }/src/conf.py (100%) rename docs/{iris => }/src/copyright.rst (100%) rename docs/{iris => }/src/developers_guide/ci_checks.png (100%) rename docs/{iris => }/src/developers_guide/contributing_changes.rst (100%) rename docs/{iris => }/src/developers_guide/contributing_ci_tests.rst (100%) rename docs/{iris => }/src/developers_guide/contributing_code_formatting.rst (100%) rename docs/{iris => }/src/developers_guide/contributing_codebase_index.rst (100%) rename docs/{iris => }/src/developers_guide/contributing_deprecations.rst (100%) rename docs/{iris => }/src/developers_guide/contributing_documentation.rst (89%) rename docs/{iris => }/src/developers_guide/contributing_getting_involved.rst (100%) rename docs/{iris => }/src/developers_guide/contributing_graphics_tests.rst (99%) rename docs/{iris => }/src/developers_guide/contributing_pull_request_checklist.rst (100%) rename docs/{iris => }/src/developers_guide/contributing_running_tests.rst (100%) rename docs/{iris => }/src/developers_guide/contributing_testing.rst (100%) rename docs/{iris => }/src/developers_guide/contributing_testing_index.rst (100%) rename docs/{iris => }/src/developers_guide/documenting/__init__.py (100%) rename docs/{iris => }/src/developers_guide/documenting/docstrings.rst (100%) rename docs/{iris => }/src/developers_guide/documenting/docstrings_attribute.py (100%) rename docs/{iris => }/src/developers_guide/documenting/docstrings_sample_routine.py (100%) rename docs/{iris => }/src/developers_guide/documenting/rest_guide.rst (100%) rename docs/{iris => }/src/developers_guide/documenting/whats_new_contributions.rst (98%) rename docs/{iris => }/src/developers_guide/gitwash/LICENSE (100%) rename docs/{iris => }/src/developers_guide/gitwash/branch_dropdown.png (100%) rename docs/{iris => }/src/developers_guide/gitwash/configure_git.rst (100%) rename docs/{iris => }/src/developers_guide/gitwash/development_workflow.rst (100%) rename docs/{iris => }/src/developers_guide/gitwash/forking.rst (100%) rename docs/{iris => }/src/developers_guide/gitwash/forking_button.png (100%) rename docs/{iris => }/src/developers_guide/gitwash/git_intro.rst (100%) rename docs/{iris => }/src/developers_guide/gitwash/git_links.inc (100%) rename docs/{iris => }/src/developers_guide/gitwash/index.rst (100%) rename docs/{iris => }/src/developers_guide/gitwash/links.inc (100%) rename docs/{iris => }/src/developers_guide/gitwash/pull_button.png (100%) rename docs/{iris => }/src/developers_guide/gitwash/set_up_fork.rst (100%) rename docs/{iris => }/src/developers_guide/release.rst (94%) rename docs/{iris => }/src/further_topics/index.rst (100%) rename docs/{iris => }/src/further_topics/lenient_maths.rst (100%) rename docs/{iris => }/src/further_topics/lenient_metadata.rst (100%) rename docs/{iris => }/src/further_topics/metadata.rst (100%) rename docs/{iris => }/src/index.rst (100%) rename docs/{iris => }/src/installing.rst (100%) rename docs/{iris => }/src/spelling_allow.txt (100%) rename docs/{iris => }/src/sphinxext/custom_class_autodoc.py (100%) rename docs/{iris => }/src/sphinxext/custom_data_autodoc.py (100%) rename docs/{iris => }/src/sphinxext/generate_package_rst.py (100%) rename docs/{iris => }/src/techpapers/change_management.rst (100%) rename docs/{iris => }/src/techpapers/index.rst (100%) rename docs/{iris => }/src/techpapers/missing_data_handling.rst (100%) rename docs/{iris => }/src/techpapers/um_files_loading.rst (100%) rename docs/{iris => }/src/userguide/change_management_goals.txt (100%) rename docs/{iris => }/src/userguide/citation.rst (100%) rename docs/{iris => }/src/userguide/code_maintenance.rst (100%) rename docs/{iris => }/src/userguide/concat.png (100%) rename docs/{iris => }/src/userguide/concat.svg (100%) rename docs/{iris => }/src/userguide/cube_diagram.dia (100%) rename docs/{iris => }/src/userguide/cube_diagram.png (100%) rename docs/{iris => }/src/userguide/cube_maths.rst (100%) rename docs/{iris => }/src/userguide/cube_statistics.rst (100%) rename docs/{iris => }/src/userguide/index.rst (100%) rename docs/{iris => }/src/userguide/interpolation_and_regridding.rst (100%) rename docs/{iris => }/src/userguide/iris_cubes.rst (100%) rename docs/{iris => }/src/userguide/loading_iris_cubes.rst (100%) rename docs/{iris => }/src/userguide/merge.png (100%) rename docs/{iris => }/src/userguide/merge.svg (100%) rename docs/{iris => }/src/userguide/merge_and_concat.png (100%) rename docs/{iris => }/src/userguide/merge_and_concat.rst (100%) rename docs/{iris => }/src/userguide/merge_and_concat.svg (100%) rename docs/{iris => }/src/userguide/multi_array.png (100%) rename docs/{iris => }/src/userguide/multi_array.svg (100%) rename docs/{iris => }/src/userguide/multi_array_to_cube.png (100%) rename docs/{iris => }/src/userguide/multi_array_to_cube.svg (100%) rename docs/{iris => }/src/userguide/navigating_a_cube.rst (100%) rename docs/{iris => }/src/userguide/plotting_a_cube.rst (100%) rename docs/{iris => }/src/userguide/plotting_examples/1d_quickplot_simple.py (100%) rename docs/{iris => }/src/userguide/plotting_examples/1d_simple.py (100%) rename docs/{iris => }/src/userguide/plotting_examples/1d_with_legend.py (100%) rename docs/{iris => }/src/userguide/plotting_examples/brewer.py (100%) rename docs/{iris => }/src/userguide/plotting_examples/cube_blockplot.py (100%) rename docs/{iris => }/src/userguide/plotting_examples/cube_brewer_cite_contourf.py (100%) rename docs/{iris => }/src/userguide/plotting_examples/cube_brewer_contourf.py (100%) rename docs/{iris => }/src/userguide/plotting_examples/cube_contour.py (100%) rename docs/{iris => }/src/userguide/plotting_examples/cube_contourf.py (100%) rename docs/{iris => }/src/userguide/real_and_lazy_data.rst (100%) rename docs/{iris => }/src/userguide/regridding_plots/interpolate_column.py (100%) rename docs/{iris => }/src/userguide/regridding_plots/regridded_to_global.py (100%) rename docs/{iris => }/src/userguide/regridding_plots/regridded_to_global_area_weighted.py (100%) rename docs/{iris => }/src/userguide/regridding_plots/regridded_to_rotated.py (100%) rename docs/{iris => }/src/userguide/regridding_plots/regridding_plot.py (100%) rename docs/{iris => }/src/userguide/saving_iris_cubes.rst (100%) rename docs/{iris => }/src/userguide/subsetting_a_cube.rst (100%) rename docs/{iris => }/src/whatsnew/1.0.rst (100%) rename docs/{iris => }/src/whatsnew/1.1.rst (100%) rename docs/{iris => }/src/whatsnew/1.10.rst (100%) rename docs/{iris => }/src/whatsnew/1.11.rst (100%) rename docs/{iris => }/src/whatsnew/1.12.rst (100%) rename docs/{iris => }/src/whatsnew/1.13.rst (100%) rename docs/{iris => }/src/whatsnew/1.2.rst (100%) rename docs/{iris => }/src/whatsnew/1.3.rst (100%) rename docs/{iris => }/src/whatsnew/1.4.rst (100%) rename docs/{iris => }/src/whatsnew/1.5.rst (100%) rename docs/{iris => }/src/whatsnew/1.6.rst (100%) rename docs/{iris => }/src/whatsnew/1.7.rst (100%) rename docs/{iris => }/src/whatsnew/1.8.rst (100%) rename docs/{iris => }/src/whatsnew/1.9.rst (100%) rename docs/{iris => }/src/whatsnew/2.0.rst (100%) rename docs/{iris => }/src/whatsnew/2.1.rst (100%) rename docs/{iris => }/src/whatsnew/2.2.rst (100%) rename docs/{iris => }/src/whatsnew/2.3.rst (99%) rename docs/{iris => }/src/whatsnew/2.4.rst (100%) rename docs/{iris => }/src/whatsnew/3.0.1.rst (100%) rename docs/{iris => }/src/whatsnew/3.0.rst (100%) rename docs/{iris => }/src/whatsnew/images/notebook_repr.png (100%) rename docs/{iris => }/src/whatsnew/images/transverse_merc.png (100%) rename docs/{iris => }/src/whatsnew/index.rst (100%) rename docs/{iris => }/src/whatsnew/latest.rst (87%) rename docs/{iris => }/src/whatsnew/latest.rst.template (100%) diff --git a/.cirrus.yml b/.cirrus.yml index d4aedd39555..971bd3b81b0 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -106,7 +106,7 @@ linux_minimal_task: << : *LINUX_TASK_TEMPLATE tests_script: - echo "[Resources]" > ${SITE_CFG} - - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} - nox --session tests @@ -136,7 +136,7 @@ linux_task: tests_script: - echo "[Resources]" > ${SITE_CFG} - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} - - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} - nox --session tests @@ -166,7 +166,7 @@ gallery_task: tests_script: - echo "[Resources]" > ${SITE_CFG} - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} - - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} - nox --session gallery @@ -197,7 +197,7 @@ doctest_task: tests_script: - echo "[Resources]" > ${SITE_CFG} - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} - - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs/iris" >> ${SITE_CFG} + - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} - mkdir -p ${MPL_RC_DIR} - echo "backend : agg" > ${MPL_RC_FILE} - echo "image.cmap : viridis" >> ${MPL_RC_FILE} diff --git a/.flake8 b/.flake8 index e313fc2ac5d..807e8c0de14 100644 --- a/.flake8 +++ b/.flake8 @@ -30,7 +30,7 @@ exclude = .eggs, build, compiled_krb, - docs/iris/src/sphinxext/*, + docs/src/sphinxext/*, tools/*, # # ignore auto-generated files diff --git a/.gitignore b/.gitignore index 618913e7ec2..4a589524d26 100644 --- a/.gitignore +++ b/.gitignore @@ -56,11 +56,11 @@ lib/iris/tests/results/imagerepo.lock *.cover # Auto generated documentation files -docs/iris/src/_build/* -docs/iris/src/generated +docs/src/_build/* +docs/src/generated # Example test results -docs/iris/iris_image_test_output/ +docs/iris_image_test_output/ # Created by editiors *~ diff --git a/.readthedocs.yml b/.readthedocs.yml index bfc8cfa72b4..b54b0f065b3 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -7,7 +7,7 @@ conda: environment: requirements/ci/readthedocs.yml sphinx: - configuration: docs/iris/src/conf.py + configuration: docs/src/conf.py fail_on_warning: false python: diff --git a/CHANGES b/CHANGES index 2364de84a4e..cdb2b64f846 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,5 @@ This file is no longer updated and is provided for historical purposes only. -Please see docs/iris/src/whatsnew/ for a changelog. +Please see docs/src/whatsnew/ for a changelog. Release 1.4 (14 June 2013) diff --git a/MANIFEST.in b/MANIFEST.in index 6f6ec445a22..99b801e8270 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,7 +11,7 @@ include requirements/*.txt # File required to build docs recursive-include docs Makefile *.js *.png *.py *.rst -prune docs/iris/build +prune docs/build # Files required to build std_names module include tools/generate_std_names.py diff --git a/README.md b/README.md index 6339491955f..0ceac7e0890 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- Iris
+ Iris

diff --git a/docs/iris/Makefile b/docs/Makefile similarity index 100% rename from docs/iris/Makefile rename to docs/Makefile diff --git a/docs/iris/gallery_code/README.rst b/docs/gallery_code/README.rst similarity index 100% rename from docs/iris/gallery_code/README.rst rename to docs/gallery_code/README.rst diff --git a/docs/iris/gallery_code/general/README.rst b/docs/gallery_code/general/README.rst similarity index 100% rename from docs/iris/gallery_code/general/README.rst rename to docs/gallery_code/general/README.rst diff --git a/docs/iris/gallery_code/general/plot_SOI_filtering.py b/docs/gallery_code/general/plot_SOI_filtering.py similarity index 100% rename from docs/iris/gallery_code/general/plot_SOI_filtering.py rename to docs/gallery_code/general/plot_SOI_filtering.py diff --git a/docs/iris/gallery_code/general/plot_anomaly_log_colouring.py b/docs/gallery_code/general/plot_anomaly_log_colouring.py similarity index 100% rename from docs/iris/gallery_code/general/plot_anomaly_log_colouring.py rename to docs/gallery_code/general/plot_anomaly_log_colouring.py diff --git a/docs/iris/gallery_code/general/plot_coriolis.py b/docs/gallery_code/general/plot_coriolis.py similarity index 100% rename from docs/iris/gallery_code/general/plot_coriolis.py rename to docs/gallery_code/general/plot_coriolis.py diff --git a/docs/iris/gallery_code/general/plot_cross_section.py b/docs/gallery_code/general/plot_cross_section.py similarity index 100% rename from docs/iris/gallery_code/general/plot_cross_section.py rename to docs/gallery_code/general/plot_cross_section.py diff --git a/docs/iris/gallery_code/general/plot_custom_aggregation.py b/docs/gallery_code/general/plot_custom_aggregation.py similarity index 100% rename from docs/iris/gallery_code/general/plot_custom_aggregation.py rename to docs/gallery_code/general/plot_custom_aggregation.py diff --git a/docs/iris/gallery_code/general/plot_custom_file_loading.py b/docs/gallery_code/general/plot_custom_file_loading.py similarity index 100% rename from docs/iris/gallery_code/general/plot_custom_file_loading.py rename to docs/gallery_code/general/plot_custom_file_loading.py diff --git a/docs/iris/gallery_code/general/plot_global_map.py b/docs/gallery_code/general/plot_global_map.py similarity index 100% rename from docs/iris/gallery_code/general/plot_global_map.py rename to docs/gallery_code/general/plot_global_map.py diff --git a/docs/iris/gallery_code/general/plot_inset.py b/docs/gallery_code/general/plot_inset.py similarity index 100% rename from docs/iris/gallery_code/general/plot_inset.py rename to docs/gallery_code/general/plot_inset.py diff --git a/docs/iris/gallery_code/general/plot_lineplot_with_legend.py b/docs/gallery_code/general/plot_lineplot_with_legend.py similarity index 100% rename from docs/iris/gallery_code/general/plot_lineplot_with_legend.py rename to docs/gallery_code/general/plot_lineplot_with_legend.py diff --git a/docs/iris/gallery_code/general/plot_polar_stereo.py b/docs/gallery_code/general/plot_polar_stereo.py similarity index 100% rename from docs/iris/gallery_code/general/plot_polar_stereo.py rename to docs/gallery_code/general/plot_polar_stereo.py diff --git a/docs/iris/gallery_code/general/plot_polynomial_fit.py b/docs/gallery_code/general/plot_polynomial_fit.py similarity index 100% rename from docs/iris/gallery_code/general/plot_polynomial_fit.py rename to docs/gallery_code/general/plot_polynomial_fit.py diff --git a/docs/iris/gallery_code/general/plot_projections_and_annotations.py b/docs/gallery_code/general/plot_projections_and_annotations.py similarity index 100% rename from docs/iris/gallery_code/general/plot_projections_and_annotations.py rename to docs/gallery_code/general/plot_projections_and_annotations.py diff --git a/docs/iris/gallery_code/general/plot_rotated_pole_mapping.py b/docs/gallery_code/general/plot_rotated_pole_mapping.py similarity index 100% rename from docs/iris/gallery_code/general/plot_rotated_pole_mapping.py rename to docs/gallery_code/general/plot_rotated_pole_mapping.py diff --git a/docs/iris/gallery_code/meteorology/README.rst b/docs/gallery_code/meteorology/README.rst similarity index 100% rename from docs/iris/gallery_code/meteorology/README.rst rename to docs/gallery_code/meteorology/README.rst diff --git a/docs/iris/gallery_code/meteorology/plot_COP_1d.py b/docs/gallery_code/meteorology/plot_COP_1d.py similarity index 100% rename from docs/iris/gallery_code/meteorology/plot_COP_1d.py rename to docs/gallery_code/meteorology/plot_COP_1d.py diff --git a/docs/iris/gallery_code/meteorology/plot_COP_maps.py b/docs/gallery_code/meteorology/plot_COP_maps.py similarity index 100% rename from docs/iris/gallery_code/meteorology/plot_COP_maps.py rename to docs/gallery_code/meteorology/plot_COP_maps.py diff --git a/docs/iris/gallery_code/meteorology/plot_TEC.py b/docs/gallery_code/meteorology/plot_TEC.py similarity index 100% rename from docs/iris/gallery_code/meteorology/plot_TEC.py rename to docs/gallery_code/meteorology/plot_TEC.py diff --git a/docs/iris/gallery_code/meteorology/plot_deriving_phenomena.py b/docs/gallery_code/meteorology/plot_deriving_phenomena.py similarity index 100% rename from docs/iris/gallery_code/meteorology/plot_deriving_phenomena.py rename to docs/gallery_code/meteorology/plot_deriving_phenomena.py diff --git a/docs/iris/gallery_code/meteorology/plot_hovmoller.py b/docs/gallery_code/meteorology/plot_hovmoller.py similarity index 100% rename from docs/iris/gallery_code/meteorology/plot_hovmoller.py rename to docs/gallery_code/meteorology/plot_hovmoller.py diff --git a/docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py b/docs/gallery_code/meteorology/plot_lagged_ensemble.py similarity index 100% rename from docs/iris/gallery_code/meteorology/plot_lagged_ensemble.py rename to docs/gallery_code/meteorology/plot_lagged_ensemble.py diff --git a/docs/iris/gallery_code/meteorology/plot_wind_speed.py b/docs/gallery_code/meteorology/plot_wind_speed.py similarity index 100% rename from docs/iris/gallery_code/meteorology/plot_wind_speed.py rename to docs/gallery_code/meteorology/plot_wind_speed.py diff --git a/docs/iris/gallery_code/oceanography/README.rst b/docs/gallery_code/oceanography/README.rst similarity index 100% rename from docs/iris/gallery_code/oceanography/README.rst rename to docs/gallery_code/oceanography/README.rst diff --git a/docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py b/docs/gallery_code/oceanography/plot_atlantic_profiles.py similarity index 100% rename from docs/iris/gallery_code/oceanography/plot_atlantic_profiles.py rename to docs/gallery_code/oceanography/plot_atlantic_profiles.py diff --git a/docs/iris/gallery_code/oceanography/plot_load_nemo.py b/docs/gallery_code/oceanography/plot_load_nemo.py similarity index 100% rename from docs/iris/gallery_code/oceanography/plot_load_nemo.py rename to docs/gallery_code/oceanography/plot_load_nemo.py diff --git a/docs/iris/gallery_code/oceanography/plot_orca_projection.py b/docs/gallery_code/oceanography/plot_orca_projection.py similarity index 100% rename from docs/iris/gallery_code/oceanography/plot_orca_projection.py rename to docs/gallery_code/oceanography/plot_orca_projection.py diff --git a/docs/iris/gallery_tests/__init__.py b/docs/gallery_tests/__init__.py similarity index 100% rename from docs/iris/gallery_tests/__init__.py rename to docs/gallery_tests/__init__.py diff --git a/docs/iris/gallery_tests/gallerytest_util.py b/docs/gallery_tests/gallerytest_util.py similarity index 100% rename from docs/iris/gallery_tests/gallerytest_util.py rename to docs/gallery_tests/gallerytest_util.py diff --git a/docs/iris/gallery_tests/test_plot_COP_1d.py b/docs/gallery_tests/test_plot_COP_1d.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_COP_1d.py rename to docs/gallery_tests/test_plot_COP_1d.py diff --git a/docs/iris/gallery_tests/test_plot_COP_maps.py b/docs/gallery_tests/test_plot_COP_maps.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_COP_maps.py rename to docs/gallery_tests/test_plot_COP_maps.py diff --git a/docs/iris/gallery_tests/test_plot_SOI_filtering.py b/docs/gallery_tests/test_plot_SOI_filtering.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_SOI_filtering.py rename to docs/gallery_tests/test_plot_SOI_filtering.py diff --git a/docs/iris/gallery_tests/test_plot_TEC.py b/docs/gallery_tests/test_plot_TEC.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_TEC.py rename to docs/gallery_tests/test_plot_TEC.py diff --git a/docs/iris/gallery_tests/test_plot_anomaly_log_colouring.py b/docs/gallery_tests/test_plot_anomaly_log_colouring.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_anomaly_log_colouring.py rename to docs/gallery_tests/test_plot_anomaly_log_colouring.py diff --git a/docs/iris/gallery_tests/test_plot_atlantic_profiles.py b/docs/gallery_tests/test_plot_atlantic_profiles.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_atlantic_profiles.py rename to docs/gallery_tests/test_plot_atlantic_profiles.py diff --git a/docs/iris/gallery_tests/test_plot_coriolis.py b/docs/gallery_tests/test_plot_coriolis.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_coriolis.py rename to docs/gallery_tests/test_plot_coriolis.py diff --git a/docs/iris/gallery_tests/test_plot_cross_section.py b/docs/gallery_tests/test_plot_cross_section.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_cross_section.py rename to docs/gallery_tests/test_plot_cross_section.py diff --git a/docs/iris/gallery_tests/test_plot_custom_aggregation.py b/docs/gallery_tests/test_plot_custom_aggregation.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_custom_aggregation.py rename to docs/gallery_tests/test_plot_custom_aggregation.py diff --git a/docs/iris/gallery_tests/test_plot_custom_file_loading.py b/docs/gallery_tests/test_plot_custom_file_loading.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_custom_file_loading.py rename to docs/gallery_tests/test_plot_custom_file_loading.py diff --git a/docs/iris/gallery_tests/test_plot_deriving_phenomena.py b/docs/gallery_tests/test_plot_deriving_phenomena.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_deriving_phenomena.py rename to docs/gallery_tests/test_plot_deriving_phenomena.py diff --git a/docs/iris/gallery_tests/test_plot_global_map.py b/docs/gallery_tests/test_plot_global_map.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_global_map.py rename to docs/gallery_tests/test_plot_global_map.py diff --git a/docs/iris/gallery_tests/test_plot_hovmoller.py b/docs/gallery_tests/test_plot_hovmoller.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_hovmoller.py rename to docs/gallery_tests/test_plot_hovmoller.py diff --git a/docs/iris/gallery_tests/test_plot_inset.py b/docs/gallery_tests/test_plot_inset.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_inset.py rename to docs/gallery_tests/test_plot_inset.py diff --git a/docs/iris/gallery_tests/test_plot_lagged_ensemble.py b/docs/gallery_tests/test_plot_lagged_ensemble.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_lagged_ensemble.py rename to docs/gallery_tests/test_plot_lagged_ensemble.py diff --git a/docs/iris/gallery_tests/test_plot_lineplot_with_legend.py b/docs/gallery_tests/test_plot_lineplot_with_legend.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_lineplot_with_legend.py rename to docs/gallery_tests/test_plot_lineplot_with_legend.py diff --git a/docs/iris/gallery_tests/test_plot_load_nemo.py b/docs/gallery_tests/test_plot_load_nemo.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_load_nemo.py rename to docs/gallery_tests/test_plot_load_nemo.py diff --git a/docs/iris/gallery_tests/test_plot_orca_projection.py b/docs/gallery_tests/test_plot_orca_projection.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_orca_projection.py rename to docs/gallery_tests/test_plot_orca_projection.py diff --git a/docs/iris/gallery_tests/test_plot_polar_stereo.py b/docs/gallery_tests/test_plot_polar_stereo.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_polar_stereo.py rename to docs/gallery_tests/test_plot_polar_stereo.py diff --git a/docs/iris/gallery_tests/test_plot_polynomial_fit.py b/docs/gallery_tests/test_plot_polynomial_fit.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_polynomial_fit.py rename to docs/gallery_tests/test_plot_polynomial_fit.py diff --git a/docs/iris/gallery_tests/test_plot_projections_and_annotations.py b/docs/gallery_tests/test_plot_projections_and_annotations.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_projections_and_annotations.py rename to docs/gallery_tests/test_plot_projections_and_annotations.py diff --git a/docs/iris/gallery_tests/test_plot_rotated_pole_mapping.py b/docs/gallery_tests/test_plot_rotated_pole_mapping.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_rotated_pole_mapping.py rename to docs/gallery_tests/test_plot_rotated_pole_mapping.py diff --git a/docs/iris/gallery_tests/test_plot_wind_speed.py b/docs/gallery_tests/test_plot_wind_speed.py similarity index 100% rename from docs/iris/gallery_tests/test_plot_wind_speed.py rename to docs/gallery_tests/test_plot_wind_speed.py diff --git a/docs/iris/src/IEP/IEP001.adoc b/docs/src/IEP/IEP001.adoc similarity index 100% rename from docs/iris/src/IEP/IEP001.adoc rename to docs/src/IEP/IEP001.adoc diff --git a/docs/iris/src/Makefile b/docs/src/Makefile similarity index 100% rename from docs/iris/src/Makefile rename to docs/src/Makefile diff --git a/docs/iris/src/_static/Iris7_1_trim_100.png b/docs/src/_static/Iris7_1_trim_100.png similarity index 100% rename from docs/iris/src/_static/Iris7_1_trim_100.png rename to docs/src/_static/Iris7_1_trim_100.png diff --git a/docs/iris/src/_static/Iris7_1_trim_full.png b/docs/src/_static/Iris7_1_trim_full.png similarity index 100% rename from docs/iris/src/_static/Iris7_1_trim_full.png rename to docs/src/_static/Iris7_1_trim_full.png diff --git a/docs/iris/src/_static/favicon.ico b/docs/src/_static/favicon.ico similarity index 100% rename from docs/iris/src/_static/favicon.ico rename to docs/src/_static/favicon.ico diff --git a/docs/iris/src/_static/iris-logo-title.png b/docs/src/_static/iris-logo-title.png similarity index 100% rename from docs/iris/src/_static/iris-logo-title.png rename to docs/src/_static/iris-logo-title.png diff --git a/docs/iris/src/_static/iris-logo-title.svg b/docs/src/_static/iris-logo-title.svg similarity index 100% rename from docs/iris/src/_static/iris-logo-title.svg rename to docs/src/_static/iris-logo-title.svg diff --git a/docs/iris/src/_static/theme_override.css b/docs/src/_static/theme_override.css similarity index 100% rename from docs/iris/src/_static/theme_override.css rename to docs/src/_static/theme_override.css diff --git a/docs/iris/src/_templates/layout.html b/docs/src/_templates/layout.html similarity index 100% rename from docs/iris/src/_templates/layout.html rename to docs/src/_templates/layout.html diff --git a/docs/iris/src/common_links.inc b/docs/src/common_links.inc similarity index 100% rename from docs/iris/src/common_links.inc rename to docs/src/common_links.inc diff --git a/docs/iris/src/conf.py b/docs/src/conf.py similarity index 100% rename from docs/iris/src/conf.py rename to docs/src/conf.py diff --git a/docs/iris/src/copyright.rst b/docs/src/copyright.rst similarity index 100% rename from docs/iris/src/copyright.rst rename to docs/src/copyright.rst diff --git a/docs/iris/src/developers_guide/ci_checks.png b/docs/src/developers_guide/ci_checks.png similarity index 100% rename from docs/iris/src/developers_guide/ci_checks.png rename to docs/src/developers_guide/ci_checks.png diff --git a/docs/iris/src/developers_guide/contributing_changes.rst b/docs/src/developers_guide/contributing_changes.rst similarity index 100% rename from docs/iris/src/developers_guide/contributing_changes.rst rename to docs/src/developers_guide/contributing_changes.rst diff --git a/docs/iris/src/developers_guide/contributing_ci_tests.rst b/docs/src/developers_guide/contributing_ci_tests.rst similarity index 100% rename from docs/iris/src/developers_guide/contributing_ci_tests.rst rename to docs/src/developers_guide/contributing_ci_tests.rst diff --git a/docs/iris/src/developers_guide/contributing_code_formatting.rst b/docs/src/developers_guide/contributing_code_formatting.rst similarity index 100% rename from docs/iris/src/developers_guide/contributing_code_formatting.rst rename to docs/src/developers_guide/contributing_code_formatting.rst diff --git a/docs/iris/src/developers_guide/contributing_codebase_index.rst b/docs/src/developers_guide/contributing_codebase_index.rst similarity index 100% rename from docs/iris/src/developers_guide/contributing_codebase_index.rst rename to docs/src/developers_guide/contributing_codebase_index.rst diff --git a/docs/iris/src/developers_guide/contributing_deprecations.rst b/docs/src/developers_guide/contributing_deprecations.rst similarity index 100% rename from docs/iris/src/developers_guide/contributing_deprecations.rst rename to docs/src/developers_guide/contributing_deprecations.rst diff --git a/docs/iris/src/developers_guide/contributing_documentation.rst b/docs/src/developers_guide/contributing_documentation.rst similarity index 89% rename from docs/iris/src/developers_guide/contributing_documentation.rst rename to docs/src/developers_guide/contributing_documentation.rst index 56d2257a55f..75e9dfe29c9 100644 --- a/docs/iris/src/developers_guide/contributing_documentation.rst +++ b/docs/src/developers_guide/contributing_documentation.rst @@ -24,7 +24,7 @@ The documentation uses specific packages that need to be present. Please see Building ~~~~~~~~ -The build can be run from the documentation directory ``iris/docs/iris/src``. +The build can be run from the documentation directory ``docs/src``. The build output for the html is found in the ``_build/html`` sub directory. When updating the documentation ensure the html build has *no errors* or @@ -58,8 +58,8 @@ Testing ~~~~~~~ There are a ways to test various aspects of the documentation. The -``make`` commands shown below can be run in the ``iris/docs/iris`` or -``iris/docs/iris/src`` directory. +``make`` commands shown below can be run in the ``docs`` or +``docs/src`` directory. Each :ref:`contributing.documentation.gallery` entry has a corresponding test. To run the tests:: @@ -107,7 +107,7 @@ or ignore the url. ``.readthedocs.yml``. -.. _conf.py: https://github.com/SciTools/iris/blob/master/docs/iris/src/conf.py +.. _conf.py: https://github.com/SciTools/iris/blob/master/docs/src/conf.py .. _contributing.documentation.api: @@ -117,14 +117,14 @@ Generating API Documentation In order to auto generate the API documentation based upon the docstrings a custom set of python scripts are used, these are located in the directory -``iris/docs/iris/src/sphinxext``. Once the ``make html`` command has been run, +``docs/src/sphinxext``. Once the ``make html`` command has been run, the output of these scripts can be found in -``iris/docs/iris/src/generated/api``. +``docs/src/generated/api``. If there is a particularly troublesome module that breaks the ``make html`` you can exclude the module from the API documentation. Add the entry to the ``exclude_modules`` tuple list in the -``iris/docs/iris/src/sphinxext/generate_package_rst.py`` file. +``docs/src/sphinxext/generate_package_rst.py`` file. .. _contributing.documentation.gallery: @@ -137,12 +137,12 @@ The Iris :ref:`sphx_glr_generated_gallery` uses a sphinx extension named that auto generates reStructuredText (rst) files based upon a gallery source directory that abides directory and filename convention. -The code for the gallery entries are in ``iris/docs/iris/gallery_code``. +The code for the gallery entries are in ``docs/gallery_code``. Each sub directory in this directory is a sub section of the gallery. The respective ``README.rst`` in each folder is included in the gallery output. For each gallery entry there must be a corresponding test script located in -``iris/docs/iris/gallery_tests``. +``docs/gallery_tests``. To add an entry to the gallery simple place your python code into the appropriate sub directory and name it with a prefix of ``plot_``. If your @@ -150,7 +150,7 @@ gallery entry does not fit into any existing sub directories then create a new directory and place it in there. The reStructuredText (rst) output of the gallery is located in -``iris/docs/iris/src/generated/gallery``. +``docs/src/generated/gallery``. For more information on the directory structure and options please see the `sphinx-gallery getting started diff --git a/docs/iris/src/developers_guide/contributing_getting_involved.rst b/docs/src/developers_guide/contributing_getting_involved.rst similarity index 100% rename from docs/iris/src/developers_guide/contributing_getting_involved.rst rename to docs/src/developers_guide/contributing_getting_involved.rst diff --git a/docs/iris/src/developers_guide/contributing_graphics_tests.rst b/docs/src/developers_guide/contributing_graphics_tests.rst similarity index 99% rename from docs/iris/src/developers_guide/contributing_graphics_tests.rst rename to docs/src/developers_guide/contributing_graphics_tests.rst index 8d8189c69b1..81ec9c0344e 100644 --- a/docs/iris/src/developers_guide/contributing_graphics_tests.rst +++ b/docs/src/developers_guide/contributing_graphics_tests.rst @@ -15,7 +15,7 @@ At present graphical tests are used in the following areas of Iris: * Module ``iris.tests.test_plot`` * Module ``iris.tests.test_quickplot`` * :ref:`sphx_glr_generated_gallery` plots contained in - ``docs/iris/gallery_tests``. + ``docs/gallery_tests``. Challenges diff --git a/docs/iris/src/developers_guide/contributing_pull_request_checklist.rst b/docs/src/developers_guide/contributing_pull_request_checklist.rst similarity index 100% rename from docs/iris/src/developers_guide/contributing_pull_request_checklist.rst rename to docs/src/developers_guide/contributing_pull_request_checklist.rst diff --git a/docs/iris/src/developers_guide/contributing_running_tests.rst b/docs/src/developers_guide/contributing_running_tests.rst similarity index 100% rename from docs/iris/src/developers_guide/contributing_running_tests.rst rename to docs/src/developers_guide/contributing_running_tests.rst diff --git a/docs/iris/src/developers_guide/contributing_testing.rst b/docs/src/developers_guide/contributing_testing.rst similarity index 100% rename from docs/iris/src/developers_guide/contributing_testing.rst rename to docs/src/developers_guide/contributing_testing.rst diff --git a/docs/iris/src/developers_guide/contributing_testing_index.rst b/docs/src/developers_guide/contributing_testing_index.rst similarity index 100% rename from docs/iris/src/developers_guide/contributing_testing_index.rst rename to docs/src/developers_guide/contributing_testing_index.rst diff --git a/docs/iris/src/developers_guide/documenting/__init__.py b/docs/src/developers_guide/documenting/__init__.py similarity index 100% rename from docs/iris/src/developers_guide/documenting/__init__.py rename to docs/src/developers_guide/documenting/__init__.py diff --git a/docs/iris/src/developers_guide/documenting/docstrings.rst b/docs/src/developers_guide/documenting/docstrings.rst similarity index 100% rename from docs/iris/src/developers_guide/documenting/docstrings.rst rename to docs/src/developers_guide/documenting/docstrings.rst diff --git a/docs/iris/src/developers_guide/documenting/docstrings_attribute.py b/docs/src/developers_guide/documenting/docstrings_attribute.py similarity index 100% rename from docs/iris/src/developers_guide/documenting/docstrings_attribute.py rename to docs/src/developers_guide/documenting/docstrings_attribute.py diff --git a/docs/iris/src/developers_guide/documenting/docstrings_sample_routine.py b/docs/src/developers_guide/documenting/docstrings_sample_routine.py similarity index 100% rename from docs/iris/src/developers_guide/documenting/docstrings_sample_routine.py rename to docs/src/developers_guide/documenting/docstrings_sample_routine.py diff --git a/docs/iris/src/developers_guide/documenting/rest_guide.rst b/docs/src/developers_guide/documenting/rest_guide.rst similarity index 100% rename from docs/iris/src/developers_guide/documenting/rest_guide.rst rename to docs/src/developers_guide/documenting/rest_guide.rst diff --git a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst b/docs/src/developers_guide/documenting/whats_new_contributions.rst similarity index 98% rename from docs/iris/src/developers_guide/documenting/whats_new_contributions.rst rename to docs/src/developers_guide/documenting/whats_new_contributions.rst index 4bd90213333..ebb553024bc 100644 --- a/docs/iris/src/developers_guide/documenting/whats_new_contributions.rst +++ b/docs/src/developers_guide/documenting/whats_new_contributions.rst @@ -11,7 +11,7 @@ The contribution should be included as part of the Iris Pull Request that introduces the change. The ``latest.rst`` and the past release notes are kept in -``docs/iris/src/whatsnew/``. If you are writing the first contribution after +``docs/src/whatsnew/``. If you are writing the first contribution after an Iris release: **create the new** ``latest.rst`` by copying the content from ``latest.rst.template`` in the same directory. diff --git a/docs/iris/src/developers_guide/gitwash/LICENSE b/docs/src/developers_guide/gitwash/LICENSE similarity index 100% rename from docs/iris/src/developers_guide/gitwash/LICENSE rename to docs/src/developers_guide/gitwash/LICENSE diff --git a/docs/iris/src/developers_guide/gitwash/branch_dropdown.png b/docs/src/developers_guide/gitwash/branch_dropdown.png similarity index 100% rename from docs/iris/src/developers_guide/gitwash/branch_dropdown.png rename to docs/src/developers_guide/gitwash/branch_dropdown.png diff --git a/docs/iris/src/developers_guide/gitwash/configure_git.rst b/docs/src/developers_guide/gitwash/configure_git.rst similarity index 100% rename from docs/iris/src/developers_guide/gitwash/configure_git.rst rename to docs/src/developers_guide/gitwash/configure_git.rst diff --git a/docs/iris/src/developers_guide/gitwash/development_workflow.rst b/docs/src/developers_guide/gitwash/development_workflow.rst similarity index 100% rename from docs/iris/src/developers_guide/gitwash/development_workflow.rst rename to docs/src/developers_guide/gitwash/development_workflow.rst diff --git a/docs/iris/src/developers_guide/gitwash/forking.rst b/docs/src/developers_guide/gitwash/forking.rst similarity index 100% rename from docs/iris/src/developers_guide/gitwash/forking.rst rename to docs/src/developers_guide/gitwash/forking.rst diff --git a/docs/iris/src/developers_guide/gitwash/forking_button.png b/docs/src/developers_guide/gitwash/forking_button.png similarity index 100% rename from docs/iris/src/developers_guide/gitwash/forking_button.png rename to docs/src/developers_guide/gitwash/forking_button.png diff --git a/docs/iris/src/developers_guide/gitwash/git_intro.rst b/docs/src/developers_guide/gitwash/git_intro.rst similarity index 100% rename from docs/iris/src/developers_guide/gitwash/git_intro.rst rename to docs/src/developers_guide/gitwash/git_intro.rst diff --git a/docs/iris/src/developers_guide/gitwash/git_links.inc b/docs/src/developers_guide/gitwash/git_links.inc similarity index 100% rename from docs/iris/src/developers_guide/gitwash/git_links.inc rename to docs/src/developers_guide/gitwash/git_links.inc diff --git a/docs/iris/src/developers_guide/gitwash/index.rst b/docs/src/developers_guide/gitwash/index.rst similarity index 100% rename from docs/iris/src/developers_guide/gitwash/index.rst rename to docs/src/developers_guide/gitwash/index.rst diff --git a/docs/iris/src/developers_guide/gitwash/links.inc b/docs/src/developers_guide/gitwash/links.inc similarity index 100% rename from docs/iris/src/developers_guide/gitwash/links.inc rename to docs/src/developers_guide/gitwash/links.inc diff --git a/docs/iris/src/developers_guide/gitwash/pull_button.png b/docs/src/developers_guide/gitwash/pull_button.png similarity index 100% rename from docs/iris/src/developers_guide/gitwash/pull_button.png rename to docs/src/developers_guide/gitwash/pull_button.png diff --git a/docs/iris/src/developers_guide/gitwash/set_up_fork.rst b/docs/src/developers_guide/gitwash/set_up_fork.rst similarity index 100% rename from docs/iris/src/developers_guide/gitwash/set_up_fork.rst rename to docs/src/developers_guide/gitwash/set_up_fork.rst diff --git a/docs/iris/src/developers_guide/release.rst b/docs/src/developers_guide/release.rst similarity index 94% rename from docs/iris/src/developers_guide/release.rst rename to docs/src/developers_guide/release.rst index 6ac3af5c75e..dd3f96b43db 100644 --- a/docs/iris/src/developers_guide/release.rst +++ b/docs/src/developers_guide/release.rst @@ -131,9 +131,9 @@ Release Steps release as it should already exist #. Update the what's new for the release: - * Copy ``docs/iris/src/whatsnew/latest.rst`` to a file named + * Copy ``docs/src/whatsnew/latest.rst`` to a file named ``v1.9.rst`` - * Delete the ``docs/iris/src/whatsnew/latest.rst`` file so it will not + * Delete the ``docs/src/whatsnew/latest.rst`` file so it will not cause an issue in the build * In ``v1.9.rst`` update the page title (first line of the file) to show the date and version in the format of ``v1.9 (DD MMM YYYY)``. For @@ -144,7 +144,7 @@ Release Steps * Add ``v1.9.rst`` to git and commit all changes, including removal of ``latest.rst`` -#. Update the what's new index ``docs/iris/src/whatsnew/index.rst`` +#. Update the what's new index ``docs/src/whatsnew/index.rst`` * Temporarily remove reference to ``latest.rst`` * Add a reference to ``v1.9.rst`` to the top of the list @@ -164,12 +164,12 @@ Post Release Steps available in the pop out menu in the bottom left corner include the new release version. If it is not present you will need to configure the versions available in the **admin** dashboard in Read The Docs -#. Copy ``docs/iris/src/whatsnew/latest.rst.template`` to - ``docs/iris/src/whatsnew/latest.rst``. This will reset +#. Copy ``docs/src/whatsnew/latest.rst.template`` to + ``docs/src/whatsnew/latest.rst``. This will reset the file with the ``unreleased`` heading and placeholders for the what's new headings #. Add back in the reference to ``latest.rst`` to the what's new index - ``docs/iris/src/whatsnew/index.rst`` + ``docs/src/whatsnew/index.rst`` #. Update ``Iris.__init__.py`` version string to show as ``1.10.dev0`` #. Merge back to master diff --git a/docs/iris/src/further_topics/index.rst b/docs/src/further_topics/index.rst similarity index 100% rename from docs/iris/src/further_topics/index.rst rename to docs/src/further_topics/index.rst diff --git a/docs/iris/src/further_topics/lenient_maths.rst b/docs/src/further_topics/lenient_maths.rst similarity index 100% rename from docs/iris/src/further_topics/lenient_maths.rst rename to docs/src/further_topics/lenient_maths.rst diff --git a/docs/iris/src/further_topics/lenient_metadata.rst b/docs/src/further_topics/lenient_metadata.rst similarity index 100% rename from docs/iris/src/further_topics/lenient_metadata.rst rename to docs/src/further_topics/lenient_metadata.rst diff --git a/docs/iris/src/further_topics/metadata.rst b/docs/src/further_topics/metadata.rst similarity index 100% rename from docs/iris/src/further_topics/metadata.rst rename to docs/src/further_topics/metadata.rst diff --git a/docs/iris/src/index.rst b/docs/src/index.rst similarity index 100% rename from docs/iris/src/index.rst rename to docs/src/index.rst diff --git a/docs/iris/src/installing.rst b/docs/src/installing.rst similarity index 100% rename from docs/iris/src/installing.rst rename to docs/src/installing.rst diff --git a/docs/iris/src/spelling_allow.txt b/docs/src/spelling_allow.txt similarity index 100% rename from docs/iris/src/spelling_allow.txt rename to docs/src/spelling_allow.txt diff --git a/docs/iris/src/sphinxext/custom_class_autodoc.py b/docs/src/sphinxext/custom_class_autodoc.py similarity index 100% rename from docs/iris/src/sphinxext/custom_class_autodoc.py rename to docs/src/sphinxext/custom_class_autodoc.py diff --git a/docs/iris/src/sphinxext/custom_data_autodoc.py b/docs/src/sphinxext/custom_data_autodoc.py similarity index 100% rename from docs/iris/src/sphinxext/custom_data_autodoc.py rename to docs/src/sphinxext/custom_data_autodoc.py diff --git a/docs/iris/src/sphinxext/generate_package_rst.py b/docs/src/sphinxext/generate_package_rst.py similarity index 100% rename from docs/iris/src/sphinxext/generate_package_rst.py rename to docs/src/sphinxext/generate_package_rst.py diff --git a/docs/iris/src/techpapers/change_management.rst b/docs/src/techpapers/change_management.rst similarity index 100% rename from docs/iris/src/techpapers/change_management.rst rename to docs/src/techpapers/change_management.rst diff --git a/docs/iris/src/techpapers/index.rst b/docs/src/techpapers/index.rst similarity index 100% rename from docs/iris/src/techpapers/index.rst rename to docs/src/techpapers/index.rst diff --git a/docs/iris/src/techpapers/missing_data_handling.rst b/docs/src/techpapers/missing_data_handling.rst similarity index 100% rename from docs/iris/src/techpapers/missing_data_handling.rst rename to docs/src/techpapers/missing_data_handling.rst diff --git a/docs/iris/src/techpapers/um_files_loading.rst b/docs/src/techpapers/um_files_loading.rst similarity index 100% rename from docs/iris/src/techpapers/um_files_loading.rst rename to docs/src/techpapers/um_files_loading.rst diff --git a/docs/iris/src/userguide/change_management_goals.txt b/docs/src/userguide/change_management_goals.txt similarity index 100% rename from docs/iris/src/userguide/change_management_goals.txt rename to docs/src/userguide/change_management_goals.txt diff --git a/docs/iris/src/userguide/citation.rst b/docs/src/userguide/citation.rst similarity index 100% rename from docs/iris/src/userguide/citation.rst rename to docs/src/userguide/citation.rst diff --git a/docs/iris/src/userguide/code_maintenance.rst b/docs/src/userguide/code_maintenance.rst similarity index 100% rename from docs/iris/src/userguide/code_maintenance.rst rename to docs/src/userguide/code_maintenance.rst diff --git a/docs/iris/src/userguide/concat.png b/docs/src/userguide/concat.png similarity index 100% rename from docs/iris/src/userguide/concat.png rename to docs/src/userguide/concat.png diff --git a/docs/iris/src/userguide/concat.svg b/docs/src/userguide/concat.svg similarity index 100% rename from docs/iris/src/userguide/concat.svg rename to docs/src/userguide/concat.svg diff --git a/docs/iris/src/userguide/cube_diagram.dia b/docs/src/userguide/cube_diagram.dia similarity index 100% rename from docs/iris/src/userguide/cube_diagram.dia rename to docs/src/userguide/cube_diagram.dia diff --git a/docs/iris/src/userguide/cube_diagram.png b/docs/src/userguide/cube_diagram.png similarity index 100% rename from docs/iris/src/userguide/cube_diagram.png rename to docs/src/userguide/cube_diagram.png diff --git a/docs/iris/src/userguide/cube_maths.rst b/docs/src/userguide/cube_maths.rst similarity index 100% rename from docs/iris/src/userguide/cube_maths.rst rename to docs/src/userguide/cube_maths.rst diff --git a/docs/iris/src/userguide/cube_statistics.rst b/docs/src/userguide/cube_statistics.rst similarity index 100% rename from docs/iris/src/userguide/cube_statistics.rst rename to docs/src/userguide/cube_statistics.rst diff --git a/docs/iris/src/userguide/index.rst b/docs/src/userguide/index.rst similarity index 100% rename from docs/iris/src/userguide/index.rst rename to docs/src/userguide/index.rst diff --git a/docs/iris/src/userguide/interpolation_and_regridding.rst b/docs/src/userguide/interpolation_and_regridding.rst similarity index 100% rename from docs/iris/src/userguide/interpolation_and_regridding.rst rename to docs/src/userguide/interpolation_and_regridding.rst diff --git a/docs/iris/src/userguide/iris_cubes.rst b/docs/src/userguide/iris_cubes.rst similarity index 100% rename from docs/iris/src/userguide/iris_cubes.rst rename to docs/src/userguide/iris_cubes.rst diff --git a/docs/iris/src/userguide/loading_iris_cubes.rst b/docs/src/userguide/loading_iris_cubes.rst similarity index 100% rename from docs/iris/src/userguide/loading_iris_cubes.rst rename to docs/src/userguide/loading_iris_cubes.rst diff --git a/docs/iris/src/userguide/merge.png b/docs/src/userguide/merge.png similarity index 100% rename from docs/iris/src/userguide/merge.png rename to docs/src/userguide/merge.png diff --git a/docs/iris/src/userguide/merge.svg b/docs/src/userguide/merge.svg similarity index 100% rename from docs/iris/src/userguide/merge.svg rename to docs/src/userguide/merge.svg diff --git a/docs/iris/src/userguide/merge_and_concat.png b/docs/src/userguide/merge_and_concat.png similarity index 100% rename from docs/iris/src/userguide/merge_and_concat.png rename to docs/src/userguide/merge_and_concat.png diff --git a/docs/iris/src/userguide/merge_and_concat.rst b/docs/src/userguide/merge_and_concat.rst similarity index 100% rename from docs/iris/src/userguide/merge_and_concat.rst rename to docs/src/userguide/merge_and_concat.rst diff --git a/docs/iris/src/userguide/merge_and_concat.svg b/docs/src/userguide/merge_and_concat.svg similarity index 100% rename from docs/iris/src/userguide/merge_and_concat.svg rename to docs/src/userguide/merge_and_concat.svg diff --git a/docs/iris/src/userguide/multi_array.png b/docs/src/userguide/multi_array.png similarity index 100% rename from docs/iris/src/userguide/multi_array.png rename to docs/src/userguide/multi_array.png diff --git a/docs/iris/src/userguide/multi_array.svg b/docs/src/userguide/multi_array.svg similarity index 100% rename from docs/iris/src/userguide/multi_array.svg rename to docs/src/userguide/multi_array.svg diff --git a/docs/iris/src/userguide/multi_array_to_cube.png b/docs/src/userguide/multi_array_to_cube.png similarity index 100% rename from docs/iris/src/userguide/multi_array_to_cube.png rename to docs/src/userguide/multi_array_to_cube.png diff --git a/docs/iris/src/userguide/multi_array_to_cube.svg b/docs/src/userguide/multi_array_to_cube.svg similarity index 100% rename from docs/iris/src/userguide/multi_array_to_cube.svg rename to docs/src/userguide/multi_array_to_cube.svg diff --git a/docs/iris/src/userguide/navigating_a_cube.rst b/docs/src/userguide/navigating_a_cube.rst similarity index 100% rename from docs/iris/src/userguide/navigating_a_cube.rst rename to docs/src/userguide/navigating_a_cube.rst diff --git a/docs/iris/src/userguide/plotting_a_cube.rst b/docs/src/userguide/plotting_a_cube.rst similarity index 100% rename from docs/iris/src/userguide/plotting_a_cube.rst rename to docs/src/userguide/plotting_a_cube.rst diff --git a/docs/iris/src/userguide/plotting_examples/1d_quickplot_simple.py b/docs/src/userguide/plotting_examples/1d_quickplot_simple.py similarity index 100% rename from docs/iris/src/userguide/plotting_examples/1d_quickplot_simple.py rename to docs/src/userguide/plotting_examples/1d_quickplot_simple.py diff --git a/docs/iris/src/userguide/plotting_examples/1d_simple.py b/docs/src/userguide/plotting_examples/1d_simple.py similarity index 100% rename from docs/iris/src/userguide/plotting_examples/1d_simple.py rename to docs/src/userguide/plotting_examples/1d_simple.py diff --git a/docs/iris/src/userguide/plotting_examples/1d_with_legend.py b/docs/src/userguide/plotting_examples/1d_with_legend.py similarity index 100% rename from docs/iris/src/userguide/plotting_examples/1d_with_legend.py rename to docs/src/userguide/plotting_examples/1d_with_legend.py diff --git a/docs/iris/src/userguide/plotting_examples/brewer.py b/docs/src/userguide/plotting_examples/brewer.py similarity index 100% rename from docs/iris/src/userguide/plotting_examples/brewer.py rename to docs/src/userguide/plotting_examples/brewer.py diff --git a/docs/iris/src/userguide/plotting_examples/cube_blockplot.py b/docs/src/userguide/plotting_examples/cube_blockplot.py similarity index 100% rename from docs/iris/src/userguide/plotting_examples/cube_blockplot.py rename to docs/src/userguide/plotting_examples/cube_blockplot.py diff --git a/docs/iris/src/userguide/plotting_examples/cube_brewer_cite_contourf.py b/docs/src/userguide/plotting_examples/cube_brewer_cite_contourf.py similarity index 100% rename from docs/iris/src/userguide/plotting_examples/cube_brewer_cite_contourf.py rename to docs/src/userguide/plotting_examples/cube_brewer_cite_contourf.py diff --git a/docs/iris/src/userguide/plotting_examples/cube_brewer_contourf.py b/docs/src/userguide/plotting_examples/cube_brewer_contourf.py similarity index 100% rename from docs/iris/src/userguide/plotting_examples/cube_brewer_contourf.py rename to docs/src/userguide/plotting_examples/cube_brewer_contourf.py diff --git a/docs/iris/src/userguide/plotting_examples/cube_contour.py b/docs/src/userguide/plotting_examples/cube_contour.py similarity index 100% rename from docs/iris/src/userguide/plotting_examples/cube_contour.py rename to docs/src/userguide/plotting_examples/cube_contour.py diff --git a/docs/iris/src/userguide/plotting_examples/cube_contourf.py b/docs/src/userguide/plotting_examples/cube_contourf.py similarity index 100% rename from docs/iris/src/userguide/plotting_examples/cube_contourf.py rename to docs/src/userguide/plotting_examples/cube_contourf.py diff --git a/docs/iris/src/userguide/real_and_lazy_data.rst b/docs/src/userguide/real_and_lazy_data.rst similarity index 100% rename from docs/iris/src/userguide/real_and_lazy_data.rst rename to docs/src/userguide/real_and_lazy_data.rst diff --git a/docs/iris/src/userguide/regridding_plots/interpolate_column.py b/docs/src/userguide/regridding_plots/interpolate_column.py similarity index 100% rename from docs/iris/src/userguide/regridding_plots/interpolate_column.py rename to docs/src/userguide/regridding_plots/interpolate_column.py diff --git a/docs/iris/src/userguide/regridding_plots/regridded_to_global.py b/docs/src/userguide/regridding_plots/regridded_to_global.py similarity index 100% rename from docs/iris/src/userguide/regridding_plots/regridded_to_global.py rename to docs/src/userguide/regridding_plots/regridded_to_global.py diff --git a/docs/iris/src/userguide/regridding_plots/regridded_to_global_area_weighted.py b/docs/src/userguide/regridding_plots/regridded_to_global_area_weighted.py similarity index 100% rename from docs/iris/src/userguide/regridding_plots/regridded_to_global_area_weighted.py rename to docs/src/userguide/regridding_plots/regridded_to_global_area_weighted.py diff --git a/docs/iris/src/userguide/regridding_plots/regridded_to_rotated.py b/docs/src/userguide/regridding_plots/regridded_to_rotated.py similarity index 100% rename from docs/iris/src/userguide/regridding_plots/regridded_to_rotated.py rename to docs/src/userguide/regridding_plots/regridded_to_rotated.py diff --git a/docs/iris/src/userguide/regridding_plots/regridding_plot.py b/docs/src/userguide/regridding_plots/regridding_plot.py similarity index 100% rename from docs/iris/src/userguide/regridding_plots/regridding_plot.py rename to docs/src/userguide/regridding_plots/regridding_plot.py diff --git a/docs/iris/src/userguide/saving_iris_cubes.rst b/docs/src/userguide/saving_iris_cubes.rst similarity index 100% rename from docs/iris/src/userguide/saving_iris_cubes.rst rename to docs/src/userguide/saving_iris_cubes.rst diff --git a/docs/iris/src/userguide/subsetting_a_cube.rst b/docs/src/userguide/subsetting_a_cube.rst similarity index 100% rename from docs/iris/src/userguide/subsetting_a_cube.rst rename to docs/src/userguide/subsetting_a_cube.rst diff --git a/docs/iris/src/whatsnew/1.0.rst b/docs/src/whatsnew/1.0.rst similarity index 100% rename from docs/iris/src/whatsnew/1.0.rst rename to docs/src/whatsnew/1.0.rst diff --git a/docs/iris/src/whatsnew/1.1.rst b/docs/src/whatsnew/1.1.rst similarity index 100% rename from docs/iris/src/whatsnew/1.1.rst rename to docs/src/whatsnew/1.1.rst diff --git a/docs/iris/src/whatsnew/1.10.rst b/docs/src/whatsnew/1.10.rst similarity index 100% rename from docs/iris/src/whatsnew/1.10.rst rename to docs/src/whatsnew/1.10.rst diff --git a/docs/iris/src/whatsnew/1.11.rst b/docs/src/whatsnew/1.11.rst similarity index 100% rename from docs/iris/src/whatsnew/1.11.rst rename to docs/src/whatsnew/1.11.rst diff --git a/docs/iris/src/whatsnew/1.12.rst b/docs/src/whatsnew/1.12.rst similarity index 100% rename from docs/iris/src/whatsnew/1.12.rst rename to docs/src/whatsnew/1.12.rst diff --git a/docs/iris/src/whatsnew/1.13.rst b/docs/src/whatsnew/1.13.rst similarity index 100% rename from docs/iris/src/whatsnew/1.13.rst rename to docs/src/whatsnew/1.13.rst diff --git a/docs/iris/src/whatsnew/1.2.rst b/docs/src/whatsnew/1.2.rst similarity index 100% rename from docs/iris/src/whatsnew/1.2.rst rename to docs/src/whatsnew/1.2.rst diff --git a/docs/iris/src/whatsnew/1.3.rst b/docs/src/whatsnew/1.3.rst similarity index 100% rename from docs/iris/src/whatsnew/1.3.rst rename to docs/src/whatsnew/1.3.rst diff --git a/docs/iris/src/whatsnew/1.4.rst b/docs/src/whatsnew/1.4.rst similarity index 100% rename from docs/iris/src/whatsnew/1.4.rst rename to docs/src/whatsnew/1.4.rst diff --git a/docs/iris/src/whatsnew/1.5.rst b/docs/src/whatsnew/1.5.rst similarity index 100% rename from docs/iris/src/whatsnew/1.5.rst rename to docs/src/whatsnew/1.5.rst diff --git a/docs/iris/src/whatsnew/1.6.rst b/docs/src/whatsnew/1.6.rst similarity index 100% rename from docs/iris/src/whatsnew/1.6.rst rename to docs/src/whatsnew/1.6.rst diff --git a/docs/iris/src/whatsnew/1.7.rst b/docs/src/whatsnew/1.7.rst similarity index 100% rename from docs/iris/src/whatsnew/1.7.rst rename to docs/src/whatsnew/1.7.rst diff --git a/docs/iris/src/whatsnew/1.8.rst b/docs/src/whatsnew/1.8.rst similarity index 100% rename from docs/iris/src/whatsnew/1.8.rst rename to docs/src/whatsnew/1.8.rst diff --git a/docs/iris/src/whatsnew/1.9.rst b/docs/src/whatsnew/1.9.rst similarity index 100% rename from docs/iris/src/whatsnew/1.9.rst rename to docs/src/whatsnew/1.9.rst diff --git a/docs/iris/src/whatsnew/2.0.rst b/docs/src/whatsnew/2.0.rst similarity index 100% rename from docs/iris/src/whatsnew/2.0.rst rename to docs/src/whatsnew/2.0.rst diff --git a/docs/iris/src/whatsnew/2.1.rst b/docs/src/whatsnew/2.1.rst similarity index 100% rename from docs/iris/src/whatsnew/2.1.rst rename to docs/src/whatsnew/2.1.rst diff --git a/docs/iris/src/whatsnew/2.2.rst b/docs/src/whatsnew/2.2.rst similarity index 100% rename from docs/iris/src/whatsnew/2.2.rst rename to docs/src/whatsnew/2.2.rst diff --git a/docs/iris/src/whatsnew/2.3.rst b/docs/src/whatsnew/2.3.rst similarity index 99% rename from docs/iris/src/whatsnew/2.3.rst rename to docs/src/whatsnew/2.3.rst index 2509242c057..693b67efbac 100644 --- a/docs/iris/src/whatsnew/2.3.rst +++ b/docs/src/whatsnew/2.3.rst @@ -238,7 +238,7 @@ Documentation ============= * Adopted a - `new colour logo for Iris `_ + `new colour logo for Iris `_ * Added a gallery example showing how to concatenate NEMO ocean model data, see :ref:`sphx_glr_generated_gallery_oceanography_plot_load_nemo.py`. diff --git a/docs/iris/src/whatsnew/2.4.rst b/docs/src/whatsnew/2.4.rst similarity index 100% rename from docs/iris/src/whatsnew/2.4.rst rename to docs/src/whatsnew/2.4.rst diff --git a/docs/iris/src/whatsnew/3.0.1.rst b/docs/src/whatsnew/3.0.1.rst similarity index 100% rename from docs/iris/src/whatsnew/3.0.1.rst rename to docs/src/whatsnew/3.0.1.rst diff --git a/docs/iris/src/whatsnew/3.0.rst b/docs/src/whatsnew/3.0.rst similarity index 100% rename from docs/iris/src/whatsnew/3.0.rst rename to docs/src/whatsnew/3.0.rst diff --git a/docs/iris/src/whatsnew/images/notebook_repr.png b/docs/src/whatsnew/images/notebook_repr.png similarity index 100% rename from docs/iris/src/whatsnew/images/notebook_repr.png rename to docs/src/whatsnew/images/notebook_repr.png diff --git a/docs/iris/src/whatsnew/images/transverse_merc.png b/docs/src/whatsnew/images/transverse_merc.png similarity index 100% rename from docs/iris/src/whatsnew/images/transverse_merc.png rename to docs/src/whatsnew/images/transverse_merc.png diff --git a/docs/iris/src/whatsnew/index.rst b/docs/src/whatsnew/index.rst similarity index 100% rename from docs/iris/src/whatsnew/index.rst rename to docs/src/whatsnew/index.rst diff --git a/docs/iris/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst similarity index 87% rename from docs/iris/src/whatsnew/latest.rst rename to docs/src/whatsnew/latest.rst index 618eeb10d61..a63c0834748 100644 --- a/docs/iris/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -69,12 +69,15 @@ This document explains the changes made to Iris for this release 📚 Documentation ================ -#. `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. (:pull:`3933`) +#. `@rcomer`_ updated the "Seasonal ensemble model plots" Gallery example. + (:pull:`3933`) -#. `@MHBalsmeier`_ described non-conda installation on Debian-based distros. (:pull:`3958`) +#. `@MHBalsmeier`_ described non-conda installation on Debian-based distros. + (:pull:`3958`) -#. `@bjlittle`_ clarified in the doc-string that :class:`~iris.coords.Coord` is now an `abstract base class`_ of - coordinates since ``v3.0.0``, and it is **not** possible to create an instance of it. (:pull:`3971`) +#. `@bjlittle`_ clarified in the doc-string that :class:`~iris.coords.Coord` + is now an `abstract base class`_ of coordinates since ``v3.0.0``, and it + is **not** possible to create an instance of it. (:pull:`3971`) 💼 Internal @@ -82,8 +85,12 @@ This document explains the changes made to Iris for this release #. `@rcomer`_ removed an old unused test file. (:pull:`3913`) +#. `@tkknight`_ moved the ``docs/iris`` directory to be in the parent + directory ``docs``. (:pull:`3975`) + #. `@jamesp`_ updated a test to the latest numpy version (:pull:`3977`) + .. comment Whatsnew author names (@github name) in alphabetical order. Note that, core dev names are automatically included by the common_links.inc: diff --git a/docs/iris/src/whatsnew/latest.rst.template b/docs/src/whatsnew/latest.rst.template similarity index 100% rename from docs/iris/src/whatsnew/latest.rst.template rename to docs/src/whatsnew/latest.rst.template diff --git a/lib/iris/tests/test_coding_standards.py b/lib/iris/tests/test_coding_standards.py index 79dff535eb8..1ab39330ef7 100644 --- a/lib/iris/tests/test_coding_standards.py +++ b/lib/iris/tests/test_coding_standards.py @@ -105,12 +105,11 @@ def test_license_headers(self): "noxfile.py", "build/*", "dist/*", - "docs/iris/gallery_code/*/*.py", - "docs/iris/src/developers_guide/documenting/*.py", - "docs/iris/src/userguide/plotting_examples/*.py", - "docs/iris/src/userguide/regridding_plots/*.py", - "docs/iris/src/developers_guide/gitwash_dumper.py", - "docs/iris/src/_build/*", + "docs/gallery_code/*/*.py", + "docs/src/developers_guide/documenting/*.py", + "docs/src/userguide/plotting_examples/*.py", + "docs/src/userguide/regridding_plots/*.py", + "docs/src/_build/*", "lib/iris/analysis/_scipy_interpolate.py", "lib/iris/fileformats/_pyke_rules/*", ) diff --git a/noxfile.py b/noxfile.py index 7bfcc73dd74..fc6175bdf0f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -250,7 +250,7 @@ def doctest(session): cache_cartopy(session) session.install("--no-deps", "--editable", ".") - session.cd("docs/iris") + session.cd("docs") session.run( "make", "clean", @@ -298,7 +298,7 @@ def linkcheck(session): cache_cartopy(session) session.install("--no-deps", "--editable", ".") - session.cd("docs/iris") + session.cd("docs") session.run( "make", "clean", From 97bc1e26bb17f4fcfa428116e3f095a60d26bd55 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Thu, 4 Feb 2021 09:38:22 +0000 Subject: [PATCH 18/23] core dev whatsnew entry (#3978) Added @jamesp to the core dev team --- docs/src/whatsnew/latest.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index a63c0834748..336e7ded852 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -25,7 +25,8 @@ This document explains the changes made to Iris for this release 📢 Announcements ================ -#. N/A +#. Congratulations to `@jamesp`_ who recently became an Iris core developer + after joining the Iris development team at the `Met Office`_. 🎉 ✨ Features @@ -76,8 +77,8 @@ This document explains the changes made to Iris for this release (:pull:`3958`) #. `@bjlittle`_ clarified in the doc-string that :class:`~iris.coords.Coord` - is now an `abstract base class`_ of coordinates since ``v3.0.0``, and it - is **not** possible to create an instance of it. (:pull:`3971`) + is now an `abstract base class`_ of coordinates since Iris ``3.0.0``, and + it is **not** possible to create an instance of it. (:pull:`3971`) 💼 Internal @@ -104,3 +105,4 @@ This document explains the changes made to Iris for this release .. _abstract base class: https://docs.python.org/3/library/abc.html .. _GitHub: https://github.com/SciTools/iris/issues/new/choose +.. _Met Office: https://www.metoffice.gov.uk/ From 3ff15a636f59d4cc3858199dfaaf83990e935154 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Thu, 4 Feb 2021 17:20:32 +0000 Subject: [PATCH 19/23] corrected syntax (#3980) --- docs/src/whatsnew/latest.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 336e7ded852..dae4da4ba67 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -32,8 +32,8 @@ This document explains the changes made to Iris for this release ✨ Features =========== -#. `@pelson`_ and `@trexfeathers`_ enhanced :meth:iris.plot.plot and - :meth:iris.quickplot.plot to automatically place the cube on the x axis if +#. `@pelson`_ and `@trexfeathers`_ enhanced :meth:`iris.plot.plot` and + :meth:`iris.quickplot.plot` to automatically place the cube on the x axis if the primary coordinate being plotted against is a vertical coordinate. E.g. ``iris.plot.plot(z_cube)`` will produce a z-vs-phenomenon plot, where before it would have produced a phenomenon-vs-z plot. (:pull:`3906`) From 1c62f079d1851083f396f246cbd2416ef6e192d6 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Mon, 8 Feb 2021 08:39:48 +0000 Subject: [PATCH 20/23] automate docs discovery of iris and python versions (#3981) * automate docs discovery of iris and python versions * update release docs + whatsnew entry --- docs/src/conf.py | 16 ++++++----- docs/src/developers_guide/release.rst | 38 +++++++++++++-------------- docs/src/whatsnew/latest.rst | 11 +++++--- docs/src/whatsnew/latest.rst.template | 4 +-- 4 files changed, 37 insertions(+), 32 deletions(-) diff --git a/docs/src/conf.py b/docs/src/conf.py index 8be849b7ceb..30e6150b394 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -43,6 +43,7 @@ def autolog(message): for item, value in os.environ.items(): autolog("[READTHEDOCS] {} = {}".format(item, value)) + # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, @@ -92,13 +93,14 @@ def autolog(message): # -- General configuration --------------------------------------------------- -# Create a variable that can be insterted in the rst "|copyright_years|". -# You can add more vairables here if needed -rst_epilog = """ -.. |copyright_years| replace:: {year_range} -""".format( - year_range="2010 - {}".format(upper_copy_year) -) +# Create a variable that can be inserted in the rst "|copyright_years|". +# You can add more variables here if needed. +rst_epilog = f""" +.. |copyright_years| replace:: 2010 - {upper_copy_year} +.. |python_version| replace:: {'.'.join([str(i) for i in sys.version_info[:3]])} +.. |iris_version| replace:: v{version} +.. |build_date| replace:: ({datetime.datetime.now().strftime('%d %b %Y')}) +""" # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom diff --git a/docs/src/developers_guide/release.rst b/docs/src/developers_guide/release.rst index dd3f96b43db..56328f910f2 100644 --- a/docs/src/developers_guide/release.rst +++ b/docs/src/developers_guide/release.rst @@ -121,36 +121,35 @@ release process is to be followed, including the merge back of changes into Maintainer Steps ---------------- -These steps assume a release for ``v1.9`` is to be created +These steps assume a release for ``v1.9`` is to be created. Release Steps ~~~~~~~~~~~~~ -#. Create the branch ``1.9.x`` on the main repo, not in a forked repo, for the - release candidate or release. The only exception is for a point/bugfix - release as it should already exist +#. Create the release feature branch ``1.9.x`` on `SciTools/iris`_. + The only exception is for a point/bugfix release, as it should already exist +#. Update the ``iris.__init__.py`` version string e.g., to ``1.9.0`` #. Update the what's new for the release: - * Copy ``docs/src/whatsnew/latest.rst`` to a file named - ``v1.9.rst`` - * Delete the ``docs/src/whatsnew/latest.rst`` file so it will not - cause an issue in the build - * In ``v1.9.rst`` update the page title (first line of the file) to show - the date and version in the format of ``v1.9 (DD MMM YYYY)``. For - example ``v1.9 (03 Aug 2020)`` + * Use git to rename ``docs/src/whatsnew/latest.rst`` to the release + version file ``v1.9.rst`` + * Use git to delete the ``docs/src/whatsnew/latest.rst.template`` file + * In ``v1.9.rst`` remove the ``[unreleased]`` caption from the page title. + Note that, the Iris version and release date are updated automatically + when the documentation is built * Review the file for correctness - * Work with the development team to create a 'highlights' section at the - top of the file, providing extra detail on notable changes - * Add ``v1.9.rst`` to git and commit all changes, including removal of - ``latest.rst`` + * Work with the development team to populate the ``Release Highlights`` + dropdown at the top of the file, which provides extra detail on notable + changes + * Use git to add and commit all changes, including removal of + ``latest.rst.template`` #. Update the what's new index ``docs/src/whatsnew/index.rst`` - * Temporarily remove reference to ``latest.rst`` + * Remove the reference to ``latest.rst`` * Add a reference to ``v1.9.rst`` to the top of the list -#. Update the ``Iris.__init__.py`` version string, to ``1.9.0`` -#. Check your changes by building the documentation and viewing the changes +#. Check your changes by building the documentation and reviewing #. Once all the above steps are complete, the release is cut, using the :guilabel:`Draft a new release` button on the `Iris release page `_ @@ -170,9 +169,10 @@ Post Release Steps new headings #. Add back in the reference to ``latest.rst`` to the what's new index ``docs/src/whatsnew/index.rst`` -#. Update ``Iris.__init__.py`` version string to show as ``1.10.dev0`` +#. Update ``iris.__init__.py`` version string to show as ``1.10.dev0`` #. Merge back to master .. _Read The Docs: https://readthedocs.org/projects/scitools-iris/builds/ +.. _SciTools/iris: https://github.com/SciTools/iris .. _tag on the SciTools/Iris: https://github.com/SciTools/iris/releases diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index dae4da4ba67..fbb98cb1e32 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -1,7 +1,7 @@ .. include:: ../common_links.inc - -************ +|iris_version| |build_date| [unreleased] +**************************************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) @@ -77,8 +77,11 @@ This document explains the changes made to Iris for this release (:pull:`3958`) #. `@bjlittle`_ clarified in the doc-string that :class:`~iris.coords.Coord` - is now an `abstract base class`_ of coordinates since Iris ``3.0.0``, and - it is **not** possible to create an instance of it. (:pull:`3971`) + is now an `abstract base class`_ since Iris ``3.0.0``, and it is **not** + possible to create an instance of it. (:pull:`3971`) + +#. `@bjlittle`_ added automated Iris version discovery for the ``latest.rst`` + in the ``whatsnew`` documentation. (:pull:`3981`) 💼 Internal diff --git a/docs/src/whatsnew/latest.rst.template b/docs/src/whatsnew/latest.rst.template index 0992a5c9bc3..de02207474b 100644 --- a/docs/src/whatsnew/latest.rst.template +++ b/docs/src/whatsnew/latest.rst.template @@ -1,7 +1,7 @@ .. include:: ../common_links.inc - -************ +|iris_version| |build_date| [unreleased] +**************************************** This document explains the changes made to Iris for this release (:doc:`View all changes `.) From a136915a8421864a4bb18dcc06bade3b324ff1f3 Mon Sep 17 00:00:00 2001 From: Patrick Peglar Date: Mon, 8 Feb 2021 11:20:36 +0000 Subject: [PATCH 21/23] Add abstract cube summary (#3987) Co-authored-by: stephen.worsley --- lib/iris/_representation.py | 273 ++++++++++++++++++ .../tests/unit/representation/__init__.py | 6 + .../representation/test_representation.py | 187 ++++++++++++ 3 files changed, 466 insertions(+) create mode 100644 lib/iris/_representation.py create mode 100644 lib/iris/tests/unit/representation/__init__.py create mode 100644 lib/iris/tests/unit/representation/test_representation.py diff --git a/lib/iris/_representation.py b/lib/iris/_representation.py new file mode 100644 index 00000000000..301f4a9a22c --- /dev/null +++ b/lib/iris/_representation.py @@ -0,0 +1,273 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Provides objects describing cube summaries. +""" + +import iris.util + + +class DimensionHeader: + def __init__(self, cube): + if cube.shape == (): + self.scalar = True + self.dim_names = [] + self.shape = [] + self.contents = ["scalar cube"] + else: + self.scalar = False + self.dim_names = [] + for dim in range(len(cube.shape)): + dim_coords = cube.coords( + contains_dimension=dim, dim_coords=True + ) + if dim_coords: + self.dim_names.append(dim_coords[0].name()) + else: + self.dim_names.append("-- ") + self.shape = list(cube.shape) + self.contents = [ + name + ": %d" % dim_len + for name, dim_len in zip(self.dim_names, self.shape) + ] + + +class FullHeader: + def __init__(self, cube, name_padding=35): + self.name = cube.name() + self.unit = cube.units + self.nameunit = "{name} / ({units})".format( + name=self.name, units=self.unit + ) + self.name_padding = name_padding + self.dimension_header = DimensionHeader(cube) + + +class CoordSummary: + def _summary_coord_extra(self, cube, coord): + # Returns the text needed to ensure this coordinate can be + # distinguished from all others with the same name. + extra = "" + similar_coords = cube.coords(coord.name()) + if len(similar_coords) > 1: + # Find all the attribute keys + keys = set() + for similar_coord in similar_coords: + keys.update(similar_coord.attributes.keys()) + # Look for any attributes that vary + vary = set() + attributes = {} + for key in keys: + for similar_coord in similar_coords: + if key not in similar_coord.attributes: + vary.add(key) + break + value = similar_coord.attributes[key] + if attributes.setdefault(key, value) != value: + vary.add(key) + break + keys = sorted(vary & set(coord.attributes.keys())) + bits = [ + "{}={!r}".format(key, coord.attributes[key]) for key in keys + ] + if bits: + extra = ", ".join(bits) + return extra + + +class VectorSummary(CoordSummary): + def __init__(self, cube, vector, iscoord): + self.name = iris.util.clip_string(vector.name()) + dims = vector.cube_dims(cube) + self.dim_chars = [ + "x" if dim in dims else "-" for dim in range(len(cube.shape)) + ] + if iscoord: + extra = self._summary_coord_extra(cube, vector) + self.extra = iris.util.clip_string(extra) + else: + self.extra = "" + + +class ScalarSummary(CoordSummary): + def __init__(self, cube, coord): + self.name = coord.name() + if ( + coord.units in ["1", "no_unit", "unknown"] + or coord.units.is_time_reference() + ): + self.unit = "" + else: + self.unit = " {!s}".format(coord.units) + coord_cell = coord.cell(0) + if isinstance(coord_cell.point, str): + self.string_type = True + self.lines = [ + iris.util.clip_string(str(item)) + for item in coord_cell.point.split("\n") + ] + self.point = None + self.bound = None + self.content = "\n".join(self.lines) + else: + self.string_type = False + self.lines = None + self.point = "{!s}".format(coord_cell.point) + coord_cell_cbound = coord_cell.bound + if coord_cell_cbound is not None: + self.bound = "({})".format( + ", ".join(str(val) for val in coord_cell_cbound) + ) + self.content = "{}{}, bound={}{}".format( + self.point, self.unit, self.bound, self.unit + ) + else: + self.bound = None + self.content = "{}{}".format(self.point, self.unit) + extra = self._summary_coord_extra(cube, coord) + self.extra = iris.util.clip_string(extra) + + +class Section: + def _init_(self): + self.contents = [] + + def is_empty(self): + return self.contents == [] + + +class VectorSection(Section): + def __init__(self, title, cube, vectors, iscoord): + self.title = title + self.contents = [ + VectorSummary(cube, vector, iscoord) for vector in vectors + ] + + +class ScalarSection(Section): + def __init__(self, title, cube, scalars): + self.title = title + self.contents = [ScalarSummary(cube, scalar) for scalar in scalars] + + +class ScalarCellMeasureSection(Section): + def __init__(self, title, cell_measures): + self.title = title + self.contents = [cm.name() for cm in cell_measures] + + +class AttributeSection(Section): + def __init__(self, title, attributes): + self.title = title + self.names = [] + self.values = [] + self.contents = [] + for name, value in sorted(attributes.items()): + value = iris.util.clip_string(str(value)) + self.names.append(name) + self.values.append(value) + content = "{}: {}".format(name, value) + self.contents.append(content) + + +class CellMethodSection(Section): + def __init__(self, title, cell_methods): + self.title = title + self.contents = [str(cm) for cm in cell_methods] + + +class CubeSummary: + def __init__(self, cube, shorten=False, name_padding=35): + self.section_indent = 5 + self.item_indent = 10 + self.extra_indent = 13 + self.shorten = shorten + self.header = FullHeader(cube, name_padding) + + # Cache the derived coords so we can rely on consistent + # object IDs. + derived_coords = cube.derived_coords + # Determine the cube coordinates that are scalar (single-valued) + # AND non-dimensioned. + dim_coords = cube.dim_coords + aux_coords = cube.aux_coords + all_coords = dim_coords + aux_coords + derived_coords + scalar_coords = [ + coord + for coord in all_coords + if not cube.coord_dims(coord) and coord.shape == (1,) + ] + # Determine the cube coordinates that are not scalar BUT + # dimensioned. + scalar_coord_ids = set(map(id, scalar_coords)) + vector_dim_coords = [ + coord for coord in dim_coords if id(coord) not in scalar_coord_ids + ] + vector_aux_coords = [ + coord for coord in aux_coords if id(coord) not in scalar_coord_ids + ] + vector_derived_coords = [ + coord + for coord in derived_coords + if id(coord) not in scalar_coord_ids + ] + + # cell measures + vector_cell_measures = [ + cm for cm in cube.cell_measures() if cm.shape != (1,) + ] + + # Ancillary Variables + vector_ancillary_variables = [av for av in cube.ancillary_variables()] + + # Sort scalar coordinates by name. + scalar_coords.sort(key=lambda coord: coord.name()) + # Sort vector coordinates by data dimension and name. + vector_dim_coords.sort( + key=lambda coord: (cube.coord_dims(coord), coord.name()) + ) + vector_aux_coords.sort( + key=lambda coord: (cube.coord_dims(coord), coord.name()) + ) + vector_derived_coords.sort( + key=lambda coord: (cube.coord_dims(coord), coord.name()) + ) + scalar_cell_measures = [ + cm for cm in cube.cell_measures() if cm.shape == (1,) + ] + + self.vector_sections = {} + + def add_vector_section(title, contents, iscoord=True): + self.vector_sections[title] = VectorSection( + title, cube, contents, iscoord + ) + + add_vector_section("Dimension coordinates:", vector_dim_coords) + add_vector_section("Auxiliary coordinates:", vector_aux_coords) + add_vector_section("Derived coordinates:", vector_derived_coords) + add_vector_section("Cell Measures:", vector_cell_measures, False) + add_vector_section( + "Ancillary Variables:", vector_ancillary_variables, False + ) + + self.scalar_sections = {} + + def add_scalar_section(section_class, title, *args): + self.scalar_sections[title] = section_class(title, *args) + + add_scalar_section( + ScalarSection, "Scalar Coordinates:", cube, scalar_coords + ) + add_scalar_section( + ScalarCellMeasureSection, + "Scalar cell measures:", + scalar_cell_measures, + ) + add_scalar_section(AttributeSection, "Attributes:", cube.attributes) + add_scalar_section( + CellMethodSection, "Cell methods:", cube.cell_methods + ) diff --git a/lib/iris/tests/unit/representation/__init__.py b/lib/iris/tests/unit/representation/__init__.py new file mode 100644 index 00000000000..e943ad149b7 --- /dev/null +++ b/lib/iris/tests/unit/representation/__init__.py @@ -0,0 +1,6 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +"""Unit tests for the :mod:`iris._representation` module.""" diff --git a/lib/iris/tests/unit/representation/test_representation.py b/lib/iris/tests/unit/representation/test_representation.py new file mode 100644 index 00000000000..212f454e707 --- /dev/null +++ b/lib/iris/tests/unit/representation/test_representation.py @@ -0,0 +1,187 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +"""Unit tests for the :mod:`iris._representation` module.""" + +import numpy as np +import iris.tests as tests +import iris._representation +from iris.cube import Cube +from iris.coords import ( + DimCoord, + AuxCoord, + CellMeasure, + AncillaryVariable, + CellMethod, +) + + +def example_cube(): + cube = Cube( + np.arange(6).reshape([3, 2]), + standard_name="air_temperature", + long_name="screen_air_temp", + var_name="airtemp", + units="K", + ) + lat = DimCoord([0, 1, 2], standard_name="latitude", units="degrees") + cube.add_dim_coord(lat, 0) + return cube + + +class Test_CubeSummary(tests.IrisTest): + def setUp(self): + self.cube = example_cube() + + def test_header(self): + rep = iris._representation.CubeSummary(self.cube) + header_left = rep.header.nameunit + header_right = rep.header.dimension_header.contents + + self.assertEqual(header_left, "air_temperature / (K)") + self.assertEqual(header_right, ["latitude: 3", "-- : 2"]) + + def test_blank_cube(self): + cube = Cube([1, 2]) + rep = iris._representation.CubeSummary(cube) + + self.assertEqual(rep.header.nameunit, "unknown / (unknown)") + self.assertEqual(rep.header.dimension_header.contents, ["-- : 2"]) + + expected_vector_sections = [ + "Dimension coordinates:", + "Auxiliary coordinates:", + "Derived coordinates:", + "Cell Measures:", + "Ancillary Variables:", + ] + self.assertEqual( + list(rep.vector_sections.keys()), expected_vector_sections + ) + for title in expected_vector_sections: + vector_section = rep.vector_sections[title] + self.assertEqual(vector_section.contents, []) + self.assertTrue(vector_section.is_empty()) + + expected_scalar_sections = [ + "Scalar Coordinates:", + "Scalar cell measures:", + "Attributes:", + "Cell methods:", + ] + + self.assertEqual( + list(rep.scalar_sections.keys()), expected_scalar_sections + ) + for title in expected_scalar_sections: + scalar_section = rep.scalar_sections[title] + self.assertEqual(scalar_section.contents, []) + self.assertTrue(scalar_section.is_empty()) + + def test_vector_coord(self): + rep = iris._representation.CubeSummary(self.cube) + dim_section = rep.vector_sections["Dimension coordinates:"] + + self.assertEqual(len(dim_section.contents), 1) + self.assertFalse(dim_section.is_empty()) + + dim_summary = dim_section.contents[0] + + name = dim_summary.name + dim_chars = dim_summary.dim_chars + extra = dim_summary.extra + + self.assertEqual(name, "latitude") + self.assertEqual(dim_chars, ["x", "-"]) + self.assertEqual(extra, "") + + def test_scalar_coord(self): + cube = self.cube + scalar_coord_no_bounds = AuxCoord([10], long_name="bar", units="K") + scalar_coord_with_bounds = AuxCoord( + [10], long_name="foo", units="K", bounds=[(5, 15)] + ) + scalar_coord_text = AuxCoord( + ["a\nb\nc"], long_name="foo", attributes={"key": "value"} + ) + cube.add_aux_coord(scalar_coord_no_bounds) + cube.add_aux_coord(scalar_coord_with_bounds) + cube.add_aux_coord(scalar_coord_text) + rep = iris._representation.CubeSummary(cube) + + scalar_section = rep.scalar_sections["Scalar Coordinates:"] + + self.assertEqual(len(scalar_section.contents), 3) + + no_bounds_summary = scalar_section.contents[0] + bounds_summary = scalar_section.contents[1] + text_summary = scalar_section.contents[2] + + self.assertEqual(no_bounds_summary.name, "bar") + self.assertEqual(no_bounds_summary.content, "10 K") + self.assertEqual(no_bounds_summary.extra, "") + + self.assertEqual(bounds_summary.name, "foo") + self.assertEqual(bounds_summary.content, "10 K, bound=(5, 15) K") + self.assertEqual(bounds_summary.extra, "") + + self.assertEqual(text_summary.name, "foo") + self.assertEqual(text_summary.content, "a\nb\nc") + self.assertEqual(text_summary.extra, "key='value'") + + def test_cell_measure(self): + cube = self.cube + cell_measure = CellMeasure([1, 2, 3], long_name="foo") + cube.add_cell_measure(cell_measure, 0) + rep = iris._representation.CubeSummary(cube) + + cm_section = rep.vector_sections["Cell Measures:"] + self.assertEqual(len(cm_section.contents), 1) + + cm_summary = cm_section.contents[0] + self.assertEqual(cm_summary.name, "foo") + self.assertEqual(cm_summary.dim_chars, ["x", "-"]) + + def test_ancillary_variable(self): + cube = self.cube + cell_measure = AncillaryVariable([1, 2, 3], long_name="foo") + cube.add_ancillary_variable(cell_measure, 0) + rep = iris._representation.CubeSummary(cube) + + av_section = rep.vector_sections["Ancillary Variables:"] + self.assertEqual(len(av_section.contents), 1) + + av_summary = av_section.contents[0] + self.assertEqual(av_summary.name, "foo") + self.assertEqual(av_summary.dim_chars, ["x", "-"]) + + def test_attributes(self): + cube = self.cube + cube.attributes = {"a": 1, "b": "two"} + rep = iris._representation.CubeSummary(cube) + + attribute_section = rep.scalar_sections["Attributes:"] + attribute_contents = attribute_section.contents + expected_contents = ["a: 1", "b: two"] + + self.assertEqual(attribute_contents, expected_contents) + + def test_cell_methods(self): + cube = self.cube + x = AuxCoord(1, long_name="x") + y = AuxCoord(1, long_name="y") + cell_method_xy = CellMethod("mean", [x, y]) + cell_method_x = CellMethod("mean", x) + cube.add_cell_method(cell_method_xy) + cube.add_cell_method(cell_method_x) + + rep = iris._representation.CubeSummary(cube) + cell_method_section = rep.scalar_sections["Cell methods:"] + expected_contents = ["mean: x, y", "mean: x"] + self.assertEqual(cell_method_section.contents, expected_contents) + + +if __name__ == "__main__": + tests.main() From 7d73cf20a75ff55230bf108bc8689f9edd24bb76 Mon Sep 17 00:00:00 2001 From: Bill Little Date: Tue, 9 Feb 2021 10:15:38 +0000 Subject: [PATCH 22/23] add nox session conda list (#3990) --- .cirrus.yml | 10 +- .../contributing_running_tests.rst | 8 + docs/src/whatsnew/latest.rst | 4 + noxfile.py | 148 +++++++----------- 4 files changed, 73 insertions(+), 97 deletions(-) diff --git a/.cirrus.yml b/.cirrus.yml index 971bd3b81b0..007bab403e3 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -107,7 +107,7 @@ linux_minimal_task: tests_script: - echo "[Resources]" > ${SITE_CFG} - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} - - nox --session tests + - nox --session tests -- --verbose # @@ -137,7 +137,7 @@ linux_task: - echo "[Resources]" > ${SITE_CFG} - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} - - nox --session tests + - nox --session tests -- --verbose # @@ -167,7 +167,7 @@ gallery_task: - echo "[Resources]" > ${SITE_CFG} - echo "test_data_dir = ${IRIS_TEST_DATA_DIR}/test_data" >> ${SITE_CFG} - echo "doc_dir = ${CIRRUS_WORKING_DIR}/docs" >> ${SITE_CFG} - - nox --session gallery + - nox --session gallery -- --verbose # @@ -201,7 +201,7 @@ doctest_task: - mkdir -p ${MPL_RC_DIR} - echo "backend : agg" > ${MPL_RC_FILE} - echo "image.cmap : viridis" >> ${MPL_RC_FILE} - - nox --session doctest + - nox --session doctest -- --verbose # @@ -224,4 +224,4 @@ link_task: - mkdir -p ${MPL_RC_DIR} - echo "backend : agg" > ${MPL_RC_FILE} - echo "image.cmap : viridis" >> ${MPL_RC_FILE} - - nox --session linkcheck + - nox --session linkcheck -- --verbose diff --git a/docs/src/developers_guide/contributing_running_tests.rst b/docs/src/developers_guide/contributing_running_tests.rst index 99ea4e831cd..0fd9fa8486d 100644 --- a/docs/src/developers_guide/contributing_running_tests.rst +++ b/docs/src/developers_guide/contributing_running_tests.rst @@ -175,6 +175,14 @@ For further `nox`_ command-line options:: nox --help +.. tip:: + For `nox`_ sessions that use the `conda`_ backend, you can use the ``-v`` or ``--verbose`` + flag to display the `nox`_ `conda`_ environment package details and environment info. + For example:: + + nox --session tests -- --verbose + + .. note:: `nox`_ will cache its testing environments in the `.nox` root ``iris`` project directory. diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index fbb98cb1e32..ed11f60719a 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -94,6 +94,10 @@ This document explains the changes made to Iris for this release #. `@jamesp`_ updated a test to the latest numpy version (:pull:`3977`) +#. `@bjlittle`_ rationalised the ``noxfile.py``, and added the ability for + each ``nox`` session to list its ``conda`` environment packages and + environment info. (:pull:`3990`) + .. comment Whatsnew author names (@github name) in alphabetical order. Note that, diff --git a/noxfile.py b/noxfile.py index fc6175bdf0f..b6f9480290e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -93,6 +93,58 @@ def cache_cartopy(session): ) +def prepare_venv(session): + """ + Create and cache the nox session conda environment, and additionally + provide conda environment package details and info. + + Note that, iris is installed into the environment using pip. + + Parameters + ---------- + session: object + A `nox.sessions.Session` object. + + Notes + ----- + See + - https://github.com/theacodes/nox/issues/346 + - https://github.com/theacodes/nox/issues/260 + + """ + if not venv_cached(session): + # Determine the conda requirements yaml file. + fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" + # Back-door approach to force nox to use "conda env update". + command = ( + "conda", + "env", + "update", + f"--prefix={session.virtualenv.location}", + f"--file={fname}", + "--prune", + ) + session._run(*command, silent=True, external="error") + cache_venv(session) + + cache_cartopy(session) + session.install("--no-deps", "--editable", ".") + + # Determine whether verbose diagnostics have been requested + # from the command line. + verbose = "-v" in session.posargs or "--verbose" in session.posargs + + if verbose: + session.run("conda", "info") + session.run("conda", "list", f"--prefix={session.virtualenv.location}") + session.run( + "conda", + "list", + f"--prefix={session.virtualenv.location}", + "--explicit", + ) + + @nox.session def flake8(session): """ @@ -141,30 +193,8 @@ def tests(session): session: object A `nox.sessions.Session` object. - Notes - ----- - See - - https://github.com/theacodes/nox/issues/346 - - https://github.com/theacodes/nox/issues/260 - """ - if not venv_cached(session): - # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" - # Back-door approach to force nox to use "conda env update". - command = ( - "conda", - "env", - "update", - f"--prefix={session.virtualenv.location}", - f"--file={fname}", - "--prune", - ) - session._run(*command, silent=True, external="error") - cache_venv(session) - - cache_cartopy(session) - session.install("--no-deps", "--editable", ".") + prepare_venv(session) session.run( "python", "-m", @@ -184,30 +214,8 @@ def gallery(session): session: object A `nox.sessions.Session` object. - Notes - ----- - See - - https://github.com/theacodes/nox/issues/346 - - https://github.com/theacodes/nox/issues/260 - """ - if not venv_cached(session): - # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" - # Back-door approach to force nox to use "conda env update". - command = ( - "conda", - "env", - "update", - f"--prefix={session.virtualenv.location}", - f"--file={fname}", - "--prune", - ) - session._run(*command, silent=True, external="error") - cache_venv(session) - - cache_cartopy(session) - session.install("--no-deps", "--editable", ".") + prepare_venv(session) session.run( "python", "-m", @@ -226,30 +234,8 @@ def doctest(session): session: object A `nox.sessions.Session` object. - Notes - ----- - See - - https://github.com/theacodes/nox/issues/346 - - https://github.com/theacodes/nox/issues/260 - """ - if not venv_cached(session): - # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" - # Back-door approach to force nox to use "conda env update". - command = ( - "conda", - "env", - "update", - f"--prefix={session.virtualenv.location}", - f"--file={fname}", - "--prune", - ) - session._run(*command, silent=True, external="error") - cache_venv(session) - - cache_cartopy(session) - session.install("--no-deps", "--editable", ".") + prepare_venv(session) session.cd("docs") session.run( "make", @@ -274,30 +260,8 @@ def linkcheck(session): session: object A `nox.sessions.Session` object. - Notes - ----- - See - - https://github.com/theacodes/nox/issues/346 - - https://github.com/theacodes/nox/issues/260 - """ - if not venv_cached(session): - # Determine the conda requirements yaml file. - fname = f"requirements/ci/py{session.python.replace('.', '')}.yml" - # Back-door approach to force nox to use "conda env update". - command = ( - "conda", - "env", - "update", - f"--prefix={session.virtualenv.location}", - f"--file={fname}", - "--prune", - ) - session._run(*command, silent=True, external="error") - cache_venv(session) - - cache_cartopy(session) - session.install("--no-deps", "--editable", ".") + prepare_venv(session) session.cd("docs") session.run( "make", From c51dab213b92b9e7eb1a95e5f650c9fec0f5b9d4 Mon Sep 17 00:00:00 2001 From: tkknight <2108488+tkknight@users.noreply.github.com> Date: Tue, 9 Feb 2021 12:14:40 +0000 Subject: [PATCH 23/23] Added text to state the Python version used to build the docs. (#3989) * Added text to state the Python version used to build the docs. * Added footer template that includes the Python version used to build. * added new line * Review actions * added whatsnew --- docs/src/_templates/footer.html | 5 +++++ docs/src/conf.py | 14 +++++++++----- .../contributing_documentation.rst | 3 +++ docs/src/installing.rst | 4 +++- docs/src/whatsnew/latest.rst | 4 ++++ 5 files changed, 24 insertions(+), 6 deletions(-) create mode 100644 docs/src/_templates/footer.html diff --git a/docs/src/_templates/footer.html b/docs/src/_templates/footer.html new file mode 100644 index 00000000000..1d5fb08b789 --- /dev/null +++ b/docs/src/_templates/footer.html @@ -0,0 +1,5 @@ +{% extends "!footer.html" %} +{% block extrafooter %} + Built using Python {{ python_version }}. + {{ super() }} +{% endblock %} diff --git a/docs/src/conf.py b/docs/src/conf.py index 30e6150b394..843af179444 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -69,8 +69,8 @@ def autolog(message): # define the copyright information for latex builds. Note, for html builds, # the copyright exists directly inside "_templates/layout.html" -upper_copy_year = datetime.datetime.now().year -copyright = "Iris Contributors" +copyright_years = f"2010 - {datetime.datetime.now().year}" +copyright = f"{copyright_years}, Iris Contributors" author = "Iris Developers" # The version info for the project you're documenting, acts as replacement for @@ -95,9 +95,12 @@ def autolog(message): # Create a variable that can be inserted in the rst "|copyright_years|". # You can add more variables here if needed. + +build_python_version = ".".join([str(i) for i in sys.version_info[:3]]) + rst_epilog = f""" -.. |copyright_years| replace:: 2010 - {upper_copy_year} -.. |python_version| replace:: {'.'.join([str(i) for i in sys.version_info[:3]])} +.. |copyright_years| replace:: {copyright_years} +.. |python_version| replace:: {build_python_version} .. |iris_version| replace:: v{version} .. |build_date| replace:: ({datetime.datetime.now().strftime('%d %b %Y')}) """ @@ -225,7 +228,8 @@ def autolog(message): } html_context = { - "copyright_years": "2010 - {}".format(upper_copy_year), + "copyright_years": copyright_years, + "python_version": build_python_version, # menu_links and menu_links_name are used in _templates/layout.html # to include some nice icons. See http://fontawesome.io for a list of # icons (used in the sphinx_rtd_theme) diff --git a/docs/src/developers_guide/contributing_documentation.rst b/docs/src/developers_guide/contributing_documentation.rst index 75e9dfe29c9..167e8937b98 100644 --- a/docs/src/developers_guide/contributing_documentation.rst +++ b/docs/src/developers_guide/contributing_documentation.rst @@ -24,6 +24,9 @@ The documentation uses specific packages that need to be present. Please see Building ~~~~~~~~ +This documentation was built using the latest Python version that Iris +supports. For more information see :ref:`installing_iris`. + The build can be run from the documentation directory ``docs/src``. The build output for the html is found in the ``_build/html`` sub directory. diff --git a/docs/src/installing.rst b/docs/src/installing.rst index 8b3ae8d3e7c..31fc497b852 100644 --- a/docs/src/installing.rst +++ b/docs/src/installing.rst @@ -17,7 +17,9 @@ any WSL_ distributions. .. _WSL: https://docs.microsoft.com/en-us/windows/wsl/install-win10 .. note:: Iris currently supports and is tested against **Python 3.6** and - **Python 3.7**. + **Python 3.7**. + +.. note:: This documentation was built using Python |python_version|. .. _installing_using_conda: diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index ed11f60719a..1efa08874a0 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -83,6 +83,10 @@ This document explains the changes made to Iris for this release #. `@bjlittle`_ added automated Iris version discovery for the ``latest.rst`` in the ``whatsnew`` documentation. (:pull:`3981`) +#. `@tkknight`_ stated the Python version used to build the documentation + on :ref:`installing_iris` and to the footer of all pages. Also added the + copyright years to the footer. (:pull:`3989`) + 💼 Internal ===========